Skip to content

Commit bccc578

Browse files
committed
Locate usable MSBuild when launching .NET Core BuildHost
1 parent 4fab744 commit bccc578

File tree

10 files changed

+105
-49
lines changed

10 files changed

+105
-49
lines changed

eng/Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
</ItemGroup>
5454

5555
<ItemGroup>
56-
<PackageVersion Include="Microsoft.Build.Locator" Version="1.6.10" />
56+
<PackageVersion Include="Microsoft.Build.Locator" Version="1.8.1" />
5757

5858
<!--
5959
Visual Studio

src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ public async ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri, bool
158158
var isFileBasedProgram = VirtualProjectXmlProvider.IsFileBasedProgram(documentPath, textAndVersion.Text);
159159

160160
const BuildHostProcessKind buildHostKind = BuildHostProcessKind.NetCore;
161-
var buildHost = await buildHostProcessManager.GetBuildHostAsync(buildHostKind, cancellationToken);
161+
var buildHost = await buildHostProcessManager.GetBuildHostAsync(buildHostKind, virtualProjectPath, dotnetPath: null, cancellationToken);
162162
var loadedFile = await buildHost.LoadProjectAsync(virtualProjectPath, virtualProjectContent, languageName: LanguageNames.CSharp, cancellationToken);
163163

164164
return new RemoteProjectLoadResult(

src/Workspaces/MSBuild/BuildHost/BuildHost.cs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ internal sealed class BuildHost : IBuildHost
2323
private readonly RpcServer _server;
2424
private readonly object _gate = new object();
2525
private ProjectBuildManager? _buildManager;
26+
private MSBuildLocation? _msBuildLocation;
2627

2728
public BuildHost(BuildHostLogger logger, ImmutableDictionary<string, string> globalMSBuildProperties, string? binaryLogPath, RpcServer server)
2829
{
@@ -32,14 +33,14 @@ public BuildHost(BuildHostLogger logger, ImmutableDictionary<string, string> glo
3233
_server = server;
3334
}
3435

35-
private bool TryEnsureMSBuildLoaded(string projectOrSolutionFilePath)
36+
public MSBuildLocation? FindUsableMSBuild(string projectOrSolutionFilePath)
3637
{
3738
lock (_gate)
3839
{
3940
// If we've already created our MSBuild types, then there's nothing further to do.
4041
if (MSBuildLocator.IsRegistered)
4142
{
42-
return true;
43+
return _msBuildLocation;
4344
}
4445

4546
if (!PlatformInformation.IsRunningOnMono)
@@ -59,14 +60,15 @@ private bool TryEnsureMSBuildLoaded(string projectOrSolutionFilePath)
5960

6061
// Locate the right SDK for this particular project; MSBuildLocator ensures in this case the first one is the preferred one.
6162
// TODO: we should pick the appropriate instance back in the main process and just use the one chosen here.
62-
var options = new VisualStudioInstanceQueryOptions { DiscoveryTypes = DiscoveryType.DotNetSdk, WorkingDirectory = Path.GetDirectoryName(projectOrSolutionFilePath) };
63+
var options = new VisualStudioInstanceQueryOptions { DiscoveryTypes = DiscoveryType.DotNetSdk, WorkingDirectory = Path.GetDirectoryName(projectOrSolutionFilePath), AllowAllDotnetLocations = true, AllowAllRuntimeVersions = true };
6364
instance = MSBuildLocator.QueryVisualStudioInstances(options).FirstOrDefault();
6465

6566
#endif
6667

6768
if (instance != null)
6869
{
6970
MSBuildLocator.RegisterInstance(instance);
71+
_msBuildLocation = new(instance.MSBuildPath, instance.Version.ToString());
7072
_logger.LogInformation($"Registered MSBuild instance at {instance.MSBuildPath}");
7173
}
7274
else
@@ -96,7 +98,7 @@ private bool TryEnsureMSBuildLoaded(string projectOrSolutionFilePath)
9698
#endif
9799
}
98100

99-
return MSBuildLocator.IsRegistered;
101+
return _msBuildLocation;
100102
}
101103
}
102104

@@ -122,14 +124,9 @@ private void CreateBuildManager()
122124
}
123125
}
124126

125-
public bool HasUsableMSBuild(string projectOrSolutionFilePath)
126-
{
127-
return TryEnsureMSBuildLoaded(projectOrSolutionFilePath);
128-
}
129-
130127
private void EnsureMSBuildLoaded(string projectFilePath)
131128
{
132-
Contract.ThrowIfFalse(TryEnsureMSBuildLoaded(projectFilePath), $"We don't have an MSBuild to use; {nameof(HasUsableMSBuild)} should have been called first to check.");
129+
Contract.ThrowIfNull(FindUsableMSBuild(projectFilePath), $"We don't have an MSBuild to use; {nameof(FindUsableMSBuild)} should have been called first to check.");
133130
}
134131

135132
/// <summary>

src/Workspaces/MSBuild/BuildHost/Rpc/Contracts/IBuildHost.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace Microsoft.CodeAnalysis.MSBuild;
1212
/// </summary>
1313
internal interface IBuildHost
1414
{
15-
bool HasUsableMSBuild(string projectOrSolutionFilePath);
15+
MSBuildLocation? FindUsableMSBuild(string projectOrSolutionFilePath);
1616
Task<int> LoadProjectFileAsync(string projectFilePath, string languageName, CancellationToken cancellationToken);
1717

1818
/// <summary>Permits loading a project file which only exists in-memory, for example, for file-based program scenarios.</summary>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Runtime.Serialization;
6+
7+
namespace Microsoft.CodeAnalysis.MSBuild;
8+
9+
[DataContract]
10+
internal sealed class MSBuildLocation(string path, string version)
11+
{
12+
[DataMember(Order = 0)]
13+
public string Path { get; } = path;
14+
15+
[DataMember(Order = 1)]
16+
public string Version { get; } = version;
17+
}

src/Workspaces/MSBuild/Core/MSBuild/BuildHostProcessManager.cs

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ internal sealed class BuildHostProcessManager : IAsyncDisposable
3333
private static string MSBuildWorkspaceDirectory => Path.GetDirectoryName(typeof(BuildHostProcessManager).Assembly.Location)!;
3434
private static bool IsLoadedFromNuGetPackage => File.Exists(Path.Combine(MSBuildWorkspaceDirectory, "..", "..", "microsoft.codeanalysis.workspaces.msbuild.nuspec"));
3535

36+
private static readonly string DotnetExecutable = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet";
37+
3638
public BuildHostProcessManager(ImmutableDictionary<string, string>? globalMSBuildProperties = null, IBinLogPathProvider? binaryLogPathProvider = null, ILoggerFactory? loggerFactory = null)
3739
{
3840
_globalMSBuildProperties = globalMSBuildProperties ?? ImmutableDictionary<string, string>.Empty;
@@ -63,58 +65,98 @@ public async Task<RemoteBuildHost> GetBuildHostWithFallbackAsync(string projectF
6365
buildHostKind = BuildHostProcessKind.NetCore;
6466
}
6567

66-
var buildHost = await GetBuildHostAsync(buildHostKind, cancellationToken).ConfigureAwait(false);
68+
var buildHost = await GetBuildHostAsync(buildHostKind, projectOrSolutionFilePath, dotnetPath: null, cancellationToken).ConfigureAwait(false);
6769

6870
// If this is a .NET Framework build host, we may not have have build tools installed and thus can't actually use it to build.
6971
// Check if this is the case. Unlike the mono case, we have to actually ask the other process since MSBuildLocator only allows
7072
// us to discover VS instances in .NET Framework hosts right now.
7173
if (buildHostKind == BuildHostProcessKind.NetFramework)
7274
{
73-
if (!await buildHost.HasUsableMSBuildAsync(projectOrSolutionFilePath, cancellationToken).ConfigureAwait(false))
75+
var msbuildLocation = await buildHost.FindUsableMSBuildAsync(projectOrSolutionFilePath, cancellationToken).ConfigureAwait(false);
76+
if (msbuildLocation is null)
7477
{
7578
// It's not usable, so we'll fall back to the .NET Core one.
7679
_logger?.LogWarning($"An installation of Visual Studio or the Build Tools for Visual Studio could not be found; {projectOrSolutionFilePath} will be loaded with the .NET Core SDK and may encounter errors.");
77-
return (await GetBuildHostAsync(BuildHostProcessKind.NetCore, cancellationToken).ConfigureAwait(false), actualKind: BuildHostProcessKind.NetCore);
80+
return await GetBuildHostWithFallbackAsync(BuildHostProcessKind.NetCore, projectOrSolutionFilePath, cancellationToken).ConfigureAwait(false);
7881
}
7982
}
8083

8184
return (buildHost, buildHostKind);
8285
}
8386

84-
public async Task<RemoteBuildHost> GetBuildHostAsync(BuildHostProcessKind buildHostKind, CancellationToken cancellationToken)
87+
public Task<RemoteBuildHost> GetBuildHostAsync(BuildHostProcessKind buildHostKind, CancellationToken cancellationToken)
88+
{
89+
return GetBuildHostAsync(buildHostKind, projectOrSolutionFilePath: null, dotnetPath: null, cancellationToken);
90+
}
91+
92+
public async Task<RemoteBuildHost> GetBuildHostAsync(BuildHostProcessKind buildHostKind, string? projectOrSolutionFilePath, string? dotnetPath, CancellationToken cancellationToken)
8593
{
8694
using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
8795
{
8896
if (!_processes.TryGetValue(buildHostKind, out var buildHostProcess))
8997
{
90-
var pipeName = Guid.NewGuid().ToString();
91-
var processStartInfo = CreateBuildHostStartInfo(buildHostKind, pipeName);
92-
93-
var process = Process.Start(processStartInfo);
94-
Contract.ThrowIfNull(process, "Process.Start failed to launch a process.");
95-
96-
buildHostProcess = new BuildHostProcess(process, pipeName, _loggerFactory);
97-
buildHostProcess.Disconnected += BuildHostProcess_Disconnected;
98-
99-
// We've subscribed to Disconnected, but if the process crashed before that point we might have not seen it
100-
if (process.HasExited)
98+
bool reload;
99+
do
101100
{
102-
buildHostProcess.LogProcessFailure();
103-
throw new Exception($"BuildHost process exited immediately with {process.ExitCode}");
104-
}
101+
reload = false;
102+
103+
var pipeName = Guid.NewGuid().ToString();
104+
var processStartInfo = CreateBuildHostStartInfo(buildHostKind, pipeName, dotnetPath);
105+
106+
var process = Process.Start(processStartInfo);
107+
Contract.ThrowIfNull(process, "Process.Start failed to launch a process.");
108+
109+
buildHostProcess = new BuildHostProcess(process, pipeName, _loggerFactory);
110+
buildHostProcess.Disconnected += BuildHostProcess_Disconnected;
111+
112+
// We've subscribed to Disconnected, but if the process crashed before that point we might have not seen it
113+
if (process.HasExited)
114+
{
115+
buildHostProcess.LogProcessFailure();
116+
throw new Exception($"BuildHost process exited immediately with {process.ExitCode}");
117+
}
118+
119+
// When running on .NET Core, we need to find the right SDK location that can load our project and restart the BuildHost if required.
120+
// When dotnetPath is null, the BuildHost is started with the default dotnet executable, which may not be the right one for the project.
121+
if (buildHostKind == BuildHostProcessKind.NetCore
122+
&& projectOrSolutionFilePath is not null
123+
&& dotnetPath is null)
124+
{
125+
// The BuildHost will be able to search through all the SDK install locations for a usable MSBuild instance.
126+
var msbuildLocation = await buildHostProcess.BuildHost.FindUsableMSBuildAsync(projectOrSolutionFilePath, cancellationToken).ConfigureAwait(false);
127+
if (msbuildLocation is not null && GetProcessPath() is { } processPath)
128+
{
129+
// The layout of the SDK is such that the dotnet executable is always at the same relative path from the MSBuild location.
130+
dotnetPath = Path.GetFullPath(Path.Combine(msbuildLocation.Path, $"../../{DotnetExecutable}"));
131+
if (dotnetPath is not null && processPath != dotnetPath)
132+
{
133+
// We need to relaunch the .NET BuildHost from a different dotnet instance.
134+
reload = true;
135+
await buildHostProcess.DisposeAsync().ConfigureAwait(false);
136+
_logger?.LogInformation($".NET BuildHost started from {processPath} reloading to start from {dotnetPath} to match necessary SDK location.");
137+
}
138+
}
139+
}
140+
} while (reload);
105141

106142
_processes.Add(buildHostKind, buildHostProcess);
107143
}
108144

109145
return buildHostProcess.BuildHost;
110146
}
147+
148+
#if NET
149+
static string? GetProcessPath() => Environment.ProcessPath;
150+
#else
151+
static string? GetProcessPath() => Process.GetCurrentProcess().MainModule?.FileName;
152+
#endif
111153
}
112154

113-
internal ProcessStartInfo CreateBuildHostStartInfo(BuildHostProcessKind buildHostKind, string pipeName)
155+
internal ProcessStartInfo CreateBuildHostStartInfo(BuildHostProcessKind buildHostKind, string pipeName, string? dotnetPath)
114156
{
115157
return buildHostKind switch
116158
{
117-
BuildHostProcessKind.NetCore => CreateDotNetCoreBuildHostStartInfo(pipeName),
159+
BuildHostProcessKind.NetCore => CreateDotNetCoreBuildHostStartInfo(pipeName, dotnetPath),
118160
BuildHostProcessKind.NetFramework => CreateDotNetFrameworkBuildHostStartInfo(pipeName),
119161
BuildHostProcessKind.Mono => CreateMonoBuildHostStartInfo(pipeName),
120162
_ => throw ExceptionUtilities.UnexpectedValue(buildHostKind)
@@ -165,11 +207,11 @@ public async ValueTask DisposeAsync()
165207
await process.DisposeAsync().ConfigureAwait(false);
166208
}
167209

168-
private ProcessStartInfo CreateDotNetCoreBuildHostStartInfo(string pipeName)
210+
private ProcessStartInfo CreateDotNetCoreBuildHostStartInfo(string pipeName, string? dotnetPath)
169211
{
170212
var processStartInfo = new ProcessStartInfo()
171213
{
172-
FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet",
214+
FileName = dotnetPath ?? DotnetExecutable,
173215
};
174216

175217
// We need to roll forward to the latest runtime, since the project may be using an SDK (or an SDK required runtime) newer than we ourselves built with.
@@ -231,9 +273,9 @@ private static string GetBuildHostPath(string contentFolderName, string assembly
231273

232274
if (IsLoadedFromNuGetPackage)
233275
{
234-
// When Workspaces.MSBuild is loaded from the NuGet package (as is the case in .NET Interactive, NCrunch, and possibly other use cases)
276+
// When Workspaces.MSBuild is loaded from the NuGet package (as is the case in .NET Interactive, NCrunch, and possibly other use cases)
235277
// the Build host is deployed under the contentFiles folder.
236-
//
278+
//
237279
// Workspaces.MSBuild.dll Path - .nuget/packages/microsoft.codeanalysis.workspaces.msbuild/{version}/lib/{tfm}/Microsoft.CodeAnalysis.Workspaces.MSBuild.dll
238280
// MSBuild.BuildHost.dll Path - .nuget/packages/microsoft.codeanalysis.workspaces.msbuild/{version}/contentFiles/any/any/{contentFolderName}/{assemblyName}
239281

src/Workspaces/MSBuild/Core/Rpc/RemoteBuildHost.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ public RemoteBuildHost(RpcClient client)
1919
_client = client;
2020
}
2121

22-
public Task<bool> HasUsableMSBuildAsync(string projectOrSolutionFilePath, CancellationToken cancellationToken)
23-
=> _client.InvokeAsync<bool>(BuildHostTargetObject, nameof(IBuildHost.HasUsableMSBuild), parameters: [projectOrSolutionFilePath], cancellationToken);
22+
public Task<MSBuildLocation> FindUsableMSBuildAsync(string projectOrSolutionFilePath, CancellationToken cancellationToken)
23+
=> _client.InvokeAsync<MSBuildLocation>(BuildHostTargetObject, nameof(IBuildHost.FindUsableMSBuild), parameters: [projectOrSolutionFilePath], cancellationToken);
2424

2525
public async Task<RemoteProjectFile> LoadProjectFileAsync(string projectFilePath, string languageName, CancellationToken cancellationToken)
2626
{

src/Workspaces/MSBuild/Test/BuildHostProcessManagerTests.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public sealed class BuildHostProcessManagerTests
1818
public void ProcessStartInfo_ForNetCore_RollsForwardToLatestPreview()
1919
{
2020
var processStartInfo = new BuildHostProcessManager()
21-
.CreateBuildHostStartInfo(BuildHostProcessKind.NetCore, pipeName: "");
21+
.CreateBuildHostStartInfo(BuildHostProcessKind.NetCore, pipeName: "", dotnetPath: null);
2222

2323
#if NET
2424
var rollForwardIndex = processStartInfo.ArgumentList.IndexOf("--roll-forward");
@@ -35,7 +35,7 @@ public void ProcessStartInfo_ForNetCore_RollsForwardToLatestPreview()
3535
public void ProcessStartInfo_ForNetCore_LaunchesDotNetCLI()
3636
{
3737
var processStartInfo = new BuildHostProcessManager()
38-
.CreateBuildHostStartInfo(BuildHostProcessKind.NetCore, pipeName: "");
38+
.CreateBuildHostStartInfo(BuildHostProcessKind.NetCore, pipeName: "", dotnetPath: null);
3939

4040
Assert.StartsWith("dotnet", processStartInfo.FileName);
4141
}
@@ -44,7 +44,7 @@ public void ProcessStartInfo_ForNetCore_LaunchesDotNetCLI()
4444
public void ProcessStartInfo_ForMono_LaunchesMono()
4545
{
4646
var processStartInfo = new BuildHostProcessManager()
47-
.CreateBuildHostStartInfo(BuildHostProcessKind.Mono, pipeName: "");
47+
.CreateBuildHostStartInfo(BuildHostProcessKind.Mono, pipeName: "", dotnetPath: null);
4848

4949
Assert.Equal("mono", processStartInfo.FileName);
5050
}
@@ -53,7 +53,7 @@ public void ProcessStartInfo_ForMono_LaunchesMono()
5353
public void ProcessStartInfo_ForNetFramework_LaunchesBuildHost()
5454
{
5555
var processStartInfo = new BuildHostProcessManager()
56-
.CreateBuildHostStartInfo(BuildHostProcessKind.NetFramework, pipeName: "");
56+
.CreateBuildHostStartInfo(BuildHostProcessKind.NetFramework, pipeName: "", dotnetPath: null);
5757

5858
Assert.EndsWith("Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.exe", processStartInfo.FileName);
5959
}
@@ -70,7 +70,7 @@ internal void ProcessStartInfo_PassesBinLogPath(BuildHostProcessKind buildHostKi
7070
binLogPathProviderMock.Setup(m => m.GetNewLogPath()).Returns(BinaryLogPath);
7171

7272
var processStartInfo = new BuildHostProcessManager(binaryLogPathProvider: binLogPathProviderMock.Object)
73-
.CreateBuildHostStartInfo(buildHostKind, pipeName: "");
73+
.CreateBuildHostStartInfo(buildHostKind, pipeName: "", dotnetPath: null);
7474

7575
#if NET
7676
var binlogIndex = processStartInfo.ArgumentList.IndexOf("--binlog");
@@ -90,7 +90,7 @@ internal void ProcessStartInfo_PassesPipeName(BuildHostProcessKind buildHostKind
9090
const string PipeName = "TestPipe";
9191

9292
var processStartInfo = new BuildHostProcessManager()
93-
.CreateBuildHostStartInfo(buildHostKind, PipeName);
93+
.CreateBuildHostStartInfo(buildHostKind, PipeName, dotnetPath: null);
9494

9595
#if NET
9696
var binlogIndex = processStartInfo.ArgumentList.IndexOf("--pipe");
@@ -111,7 +111,7 @@ internal void ProcessStartInfo_PassesLocale(BuildHostProcessKind buildHostKind)
111111
const string Locale = "de-DE";
112112

113113
var processStartInfo = new BuildHostProcessManager()
114-
.CreateBuildHostStartInfo(buildHostKind, pipeName: "");
114+
.CreateBuildHostStartInfo(buildHostKind, pipeName: "", dotnetPath: null);
115115

116116
#if NET
117117
var localeIndex = processStartInfo.ArgumentList.IndexOf("--locale");
@@ -136,7 +136,7 @@ internal void ProcessStartInfo_PassesProperties(BuildHostProcessKind buildHostKi
136136

137137
var buildHostProcessManager = new BuildHostProcessManager(globalMSBuildProperties);
138138

139-
var processStartInfo = buildHostProcessManager.CreateBuildHostStartInfo(buildHostKind, pipeName: "");
139+
var processStartInfo = buildHostProcessManager.CreateBuildHostStartInfo(buildHostKind, pipeName: "", dotnetPath: null);
140140

141141
#if NET
142142
foreach (var kvp in globalMSBuildProperties)

src/Workspaces/MSBuild/Test/Utilities/DotNetSdkMSBuildInstalled.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ private static bool HasNetCoreSdkForSolution(string solution)
4444

4545
var buildHost = buildHostProcessManager.GetBuildHostAsync(BuildHostProcessManager.BuildHostProcessKind.NetCore, CancellationToken.None).Result;
4646

47-
return buildHost.HasUsableMSBuildAsync(solution, CancellationToken.None).Result;
47+
return buildHost.FindUsableMSBuildAsync(solution, CancellationToken.None).Result is not null;
4848
}
4949
finally
5050
{

0 commit comments

Comments
 (0)