@@ -33,6 +33,8 @@ internal sealed class BuildHostProcessManager : IAsyncDisposable
33
33
private static string MSBuildWorkspaceDirectory => Path . GetDirectoryName ( typeof ( BuildHostProcessManager ) . Assembly . Location ) ! ;
34
34
private static bool IsLoadedFromNuGetPackage => File . Exists ( Path . Combine ( MSBuildWorkspaceDirectory , ".." , ".." , "microsoft.codeanalysis.workspaces.msbuild.nuspec" ) ) ;
35
35
36
+ private static readonly string DotnetExecutable = RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) ? "dotnet.exe" : "dotnet" ;
37
+
36
38
public BuildHostProcessManager ( ImmutableDictionary < string , string > ? globalMSBuildProperties = null , IBinLogPathProvider ? binaryLogPathProvider = null , ILoggerFactory ? loggerFactory = null )
37
39
{
38
40
_globalMSBuildProperties = globalMSBuildProperties ?? ImmutableDictionary < string , string > . Empty ;
@@ -63,58 +65,98 @@ public async Task<RemoteBuildHost> GetBuildHostWithFallbackAsync(string projectF
63
65
buildHostKind = BuildHostProcessKind . NetCore ;
64
66
}
65
67
66
- var buildHost = await GetBuildHostAsync ( buildHostKind , cancellationToken ) . ConfigureAwait ( false ) ;
68
+ var buildHost = await GetBuildHostAsync ( buildHostKind , projectOrSolutionFilePath , dotnetPath : null , cancellationToken ) . ConfigureAwait ( false ) ;
67
69
68
70
// 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.
69
71
// Check if this is the case. Unlike the mono case, we have to actually ask the other process since MSBuildLocator only allows
70
72
// us to discover VS instances in .NET Framework hosts right now.
71
73
if ( buildHostKind == BuildHostProcessKind . NetFramework )
72
74
{
73
- if ( ! await buildHost . HasUsableMSBuildAsync ( projectOrSolutionFilePath , cancellationToken ) . ConfigureAwait ( false ) )
75
+ var msbuildLocation = await buildHost . FindUsableMSBuildAsync ( projectOrSolutionFilePath , cancellationToken ) . ConfigureAwait ( false ) ;
76
+ if ( msbuildLocation is null )
74
77
{
75
78
// It's not usable, so we'll fall back to the .NET Core one.
76
79
_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 ) ;
78
81
}
79
82
}
80
83
81
84
return ( buildHost , buildHostKind ) ;
82
85
}
83
86
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 )
85
93
{
86
94
using ( await _gate . DisposableWaitAsync ( cancellationToken ) . ConfigureAwait ( false ) )
87
95
{
88
96
if ( ! _processes . TryGetValue ( buildHostKind , out var buildHostProcess ) )
89
97
{
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
101
100
{
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 ) ;
105
141
106
142
_processes . Add ( buildHostKind , buildHostProcess ) ;
107
143
}
108
144
109
145
return buildHostProcess . BuildHost ;
110
146
}
147
+
148
+ #if NET
149
+ static string ? GetProcessPath ( ) => Environment . ProcessPath ;
150
+ #else
151
+ static string ? GetProcessPath ( ) => Process . GetCurrentProcess ( ) . MainModule ? . FileName ;
152
+ #endif
111
153
}
112
154
113
- internal ProcessStartInfo CreateBuildHostStartInfo ( BuildHostProcessKind buildHostKind , string pipeName )
155
+ internal ProcessStartInfo CreateBuildHostStartInfo ( BuildHostProcessKind buildHostKind , string pipeName , string ? dotnetPath )
114
156
{
115
157
return buildHostKind switch
116
158
{
117
- BuildHostProcessKind . NetCore => CreateDotNetCoreBuildHostStartInfo ( pipeName ) ,
159
+ BuildHostProcessKind . NetCore => CreateDotNetCoreBuildHostStartInfo ( pipeName , dotnetPath ) ,
118
160
BuildHostProcessKind . NetFramework => CreateDotNetFrameworkBuildHostStartInfo ( pipeName ) ,
119
161
BuildHostProcessKind . Mono => CreateMonoBuildHostStartInfo ( pipeName ) ,
120
162
_ => throw ExceptionUtilities . UnexpectedValue ( buildHostKind )
@@ -165,11 +207,11 @@ public async ValueTask DisposeAsync()
165
207
await process . DisposeAsync ( ) . ConfigureAwait ( false ) ;
166
208
}
167
209
168
- private ProcessStartInfo CreateDotNetCoreBuildHostStartInfo ( string pipeName )
210
+ private ProcessStartInfo CreateDotNetCoreBuildHostStartInfo ( string pipeName , string ? dotnetPath )
169
211
{
170
212
var processStartInfo = new ProcessStartInfo ( )
171
213
{
172
- FileName = RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) ? "dotnet.exe" : "dotnet" ,
214
+ FileName = dotnetPath ?? DotnetExecutable ,
173
215
} ;
174
216
175
217
// 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
231
273
232
274
if ( IsLoadedFromNuGetPackage )
233
275
{
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)
235
277
// the Build host is deployed under the contentFiles folder.
236
- //
278
+ //
237
279
// Workspaces.MSBuild.dll Path - .nuget/packages/microsoft.codeanalysis.workspaces.msbuild/{version}/lib/{tfm}/Microsoft.CodeAnalysis.Workspaces.MSBuild.dll
238
280
// MSBuild.BuildHost.dll Path - .nuget/packages/microsoft.codeanalysis.workspaces.msbuild/{version}/contentFiles/any/any/{contentFolderName}/{assemblyName}
239
281
0 commit comments