Skip to content

Commit f34e305

Browse files
committed
Incorporated PR feedback
- Restored removed BuildHost methods. - Separated MSBuild instance finding from loading - Removed loop in favor of a single relaunch
1 parent 91bdff8 commit f34e305

File tree

7 files changed

+182
-107
lines changed

7 files changed

+182
-107
lines changed

src/Workspaces/MSBuild/BuildHost/BuildHost.cs

Lines changed: 70 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ 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;
2726

2827
public BuildHost(BuildHostLogger logger, ImmutableDictionary<string, string> globalMSBuildProperties, string? binaryLogPath, RpcServer server)
2928
{
@@ -33,73 +32,94 @@ public BuildHost(BuildHostLogger logger, ImmutableDictionary<string, string> glo
3332
_server = server;
3433
}
3534

36-
public MSBuildLocation? FindUsableMSBuild(string projectOrSolutionFilePath)
35+
public MSBuildLocation? FindBestMSBuild(string projectOrSolutionFilePath)
3736
{
38-
lock (_gate)
39-
{
40-
// If we've already created our MSBuild types, then there's nothing further to do.
41-
if (MSBuildLocator.IsRegistered)
42-
{
43-
return _msBuildLocation;
44-
}
45-
46-
if (!PlatformInformation.IsRunningOnMono)
47-
{
37+
return FindMSBuild(projectOrSolutionFilePath, includeUnloadableInstances: true);
38+
}
4839

49-
VisualStudioInstance? instance;
40+
private MSBuildLocation? FindMSBuild(string projectOrSolutionFilePath, bool includeUnloadableInstances)
41+
{
42+
if (!PlatformInformation.IsRunningOnMono)
43+
{
44+
VisualStudioInstance? instance;
5045

5146
#if NETFRAMEWORK
47+
// In this case, we're just going to pick the highest VS install on the machine, in case the projects are using some newer
48+
// MSBuild features. Since we don't have something like a global.json we can't really know what the minimum version is.
5249

53-
// In this case, we're just going to pick the highest VS install on the machine, in case the projects are using some newer
54-
// MSBuild features. Since we don't have something like a global.json we can't really know what the minimum version is.
55-
56-
// TODO: we should also check that the managed tools are actually installed
57-
instance = MSBuildLocator.QueryVisualStudioInstances().OrderByDescending(vs => vs.Version).FirstOrDefault();
58-
50+
// TODO: we should also check that the managed tools are actually installed
51+
instance = MSBuildLocator.QueryVisualStudioInstances().OrderByDescending(vs => vs.Version).FirstOrDefault();
5952
#else
53+
// Locate the right SDK for this particular project; MSBuildLocator ensures in this case the first one is the preferred one.
54+
// The includeUnloadableInstance parameter additionally locates SDKs from all installations regardless of whether they are
55+
// loadable by the BuildHost process.
56+
var options = new VisualStudioInstanceQueryOptions
57+
{
58+
DiscoveryTypes = DiscoveryType.DotNetSdk,
59+
WorkingDirectory = Path.GetDirectoryName(projectOrSolutionFilePath),
60+
AllowAllDotnetLocations = includeUnloadableInstances,
61+
AllowAllRuntimeVersions = includeUnloadableInstances,
62+
};
6063

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

68-
if (instance != null)
69-
{
70-
MSBuildLocator.RegisterInstance(instance);
71-
_msBuildLocation = new(instance.MSBuildPath, instance.Version.ToString());
72-
_logger.LogInformation($"Registered MSBuild instance at {instance.MSBuildPath}");
73-
}
74-
else
75-
{
76-
_logger.LogCritical("No compatible MSBuild instance could be found.");
77-
}
78-
}
79-
else
67+
if (instance != null)
8068
{
81-
#if NETFRAMEWORK
69+
return new(instance.MSBuildPath, instance.Version.ToString());
70+
}
8271

83-
// We're running on Mono, but not all Mono installations have a usable MSBuild installation, so let's see if we have one that we can use.
84-
var monoMSBuildDirectory = MonoMSBuildDiscovery.GetMonoMSBuildDirectory();
72+
_logger.LogCritical("No compatible MSBuild instance could be found.");
73+
}
74+
else
75+
{
8576

86-
if (monoMSBuildDirectory != null)
87-
{
88-
MSBuildLocator.RegisterMSBuildPath(monoMSBuildDirectory);
89-
_msBuildLocation = new(monoMSBuildDirectory, MonoMSBuildDiscovery.GetMonoMSBuildVersion());
90-
_logger.LogInformation($"Registered MSBuild instance at {monoMSBuildDirectory}");
91-
}
92-
else
77+
#if NETFRAMEWORK
78+
// We're running on Mono, but not all Mono installations have a usable MSBuild installation, so let's see if we have one that we can use.
79+
var monoMSBuildDirectory = MonoMSBuildDiscovery.GetMonoMSBuildDirectory();
80+
if (monoMSBuildDirectory != null)
81+
{
82+
var monoMSBuildVersion = MonoMSBuildDiscovery.GetMonoMSBuildVersion();
83+
if (monoMSBuildVersion != null)
9384
{
94-
_logger.LogCritical("No Mono MSBuild installation could be found; see https://www.mono-project.com/ for installation instructions.");
85+
return new(monoMSBuildDirectory, monoMSBuildVersion);
9586
}
87+
}
9688

89+
_logger.LogCritical("No Mono MSBuild installation could be found; see https://www.mono-project.com/ for installation instructions.");
9790
#else
98-
_logger.LogCritical("Trying to run the .NET Core BuildHost on Mono is unsupported.");
91+
_logger.LogCritical("Trying to run the .NET Core BuildHost on Mono is unsupported.");
9992
#endif
93+
94+
}
95+
96+
return null;
97+
}
98+
99+
public bool HasUsableMSBuild(string projectOrSolutionFilePath)
100+
{
101+
return TryEnsureMSBuildLoaded(projectOrSolutionFilePath);
102+
}
103+
104+
private bool TryEnsureMSBuildLoaded(string projectOrSolutionFilePath)
105+
{
106+
lock (_gate)
107+
{
108+
// If we've already created our MSBuild types, then there's nothing further to do.
109+
if (MSBuildLocator.IsRegistered)
110+
{
111+
return true;
112+
}
113+
114+
var instance = FindMSBuild(projectOrSolutionFilePath, includeUnloadableInstances: false);
115+
if (instance is null)
116+
{
117+
return false;
100118
}
101119

102-
return _msBuildLocation;
120+
MSBuildLocator.RegisterMSBuildPath(instance.Path);
121+
_logger.LogInformation($"Registered MSBuild {instance.Version} instance at {instance.Path}");
122+
return true;
103123
}
104124
}
105125

@@ -127,7 +147,7 @@ private void CreateBuildManager()
127147

128148
private void EnsureMSBuildLoaded(string projectFilePath)
129149
{
130-
Contract.ThrowIfNull(FindUsableMSBuild(projectFilePath), $"We don't have an MSBuild to use; {nameof(FindUsableMSBuild)} should have been called first to check.");
150+
Contract.ThrowIfTrue(TryEnsureMSBuildLoaded(projectFilePath), $"We don't have an MSBuild to use; {nameof(TryEnsureMSBuildLoaded)} should have been called first to check.");
131151
}
132152

133153
/// <summary>

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

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,27 @@ namespace Microsoft.CodeAnalysis.MSBuild;
1212
/// </summary>
1313
internal interface IBuildHost
1414
{
15-
MSBuildLocation? FindUsableMSBuild(string projectOrSolutionFilePath);
15+
/// <summary>
16+
/// Finds the best MSBuild instance installed for loading the given project or solution.
17+
/// </summary>
18+
/// <remarks>
19+
/// This may return MSBuild instances that are not loadable by the BuildHost process.
20+
/// </remarks>
21+
MSBuildLocation? FindBestMSBuild(string projectOrSolutionFilePath);
22+
23+
/// <summary>
24+
/// Determines whether there is a MSBuild instance that is loadable by the BuildHost process.
25+
/// </summary>
26+
/// <remarks>
27+
/// This may return true even if the project or solution require a newer version of MSBuild.
28+
/// </remarks>
29+
bool HasUsableMSBuild(string projectOrSolutionFilePath);
30+
1631
Task<int> LoadProjectFileAsync(string projectFilePath, string languageName, CancellationToken cancellationToken);
1732

18-
/// <summary>Permits loading a project file which only exists in-memory, for example, for file-based program scenarios.</summary>
33+
/// <summary>
34+
/// Permits loading a project file which only exists in-memory, for example, for file-based program scenarios.
35+
/// </summary>
1936
/// <param name="projectFilePath">A path to a project file which may or may not exist on disk. Note that an extension that is known by MSBuild, such as .csproj or .vbproj, should be used here.</param>
2037
/// <param name="projectContent">The project file XML content.</param>
2138
int LoadProject(string projectFilePath, string projectContent, string languageName);

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,18 @@ namespace Microsoft.CodeAnalysis.MSBuild;
99
[DataContract]
1010
internal sealed class MSBuildLocation(string path, string version)
1111
{
12+
/// <summary>
13+
/// This is the path to the directory containing the MSBuild binaries.
14+
/// </summary>
15+
/// <remarks>
16+
/// When running on .NET this will be the path to the SDK required for loading projects.
17+
/// </remarks>
1218
[DataMember(Order = 0)]
1319
public string Path { get; } = path;
1420

21+
/// <summary>
22+
/// This is the version of MSBuild at this location.
23+
/// </summary>
1524
[DataMember(Order = 1)]
1625
public string Version { get; } = version;
1726
}

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

Lines changed: 62 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ public async Task<RemoteBuildHost> GetBuildHostWithFallbackAsync(string projectF
5959
/// </summary>
6060
public async Task<(RemoteBuildHost buildHost, BuildHostProcessKind actualKind)> GetBuildHostWithFallbackAsync(BuildHostProcessKind buildHostKind, string projectOrSolutionFilePath, CancellationToken cancellationToken)
6161
{
62-
if (buildHostKind == BuildHostProcessKind.Mono && MonoMSBuildDiscovery.GetMonoMSBuildDirectory() == null)
62+
if (buildHostKind == BuildHostProcessKind.Mono && MonoMSBuildDiscovery.GetMonoMSBuildVersion() == null)
6363
{
64-
_logger?.LogWarning($"An installation of Mono could not be found; {projectOrSolutionFilePath} will be loaded with the .NET Core SDK and may encounter errors.");
64+
_logger?.LogWarning($"An installation of Mono MSBuild could not be found; {projectOrSolutionFilePath} will be loaded with the .NET Core SDK and may encounter errors.");
6565
buildHostKind = BuildHostProcessKind.NetCore;
6666
}
6767

@@ -72,12 +72,11 @@ public async Task<RemoteBuildHost> GetBuildHostWithFallbackAsync(string projectF
7272
// us to discover VS instances in .NET Framework hosts right now.
7373
if (buildHostKind == BuildHostProcessKind.NetFramework)
7474
{
75-
var msbuildLocation = await buildHost.FindUsableMSBuildAsync(projectOrSolutionFilePath, cancellationToken).ConfigureAwait(false);
76-
if (msbuildLocation is null)
75+
if (!await buildHost.HasUsableMSBuildAsync(projectOrSolutionFilePath, cancellationToken).ConfigureAwait(false))
7776
{
7877
// It's not usable, so we'll fall back to the .NET Core one.
7978
_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.");
80-
return await GetBuildHostWithFallbackAsync(BuildHostProcessKind.NetCore, projectOrSolutionFilePath, cancellationToken).ConfigureAwait(false);
79+
return (await GetBuildHostAsync(BuildHostProcessKind.NetCore, projectOrSolutionFilePath, dotnetPath: null, cancellationToken).ConfigureAwait(false), BuildHostProcessKind.NetCore);
8180
}
8281
}
8382

@@ -95,60 +94,73 @@ public async Task<RemoteBuildHost> GetBuildHostAsync(BuildHostProcessKind buildH
9594
{
9695
if (!_processes.TryGetValue(buildHostKind, out var buildHostProcess))
9796
{
98-
bool reload;
99-
do
100-
{
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);
97+
buildHostProcess = await NoLock_GetBuildHostAsync(buildHostKind, projectOrSolutionFilePath, dotnetPath, cancellationToken).ConfigureAwait(false);
14198

14299
_processes.Add(buildHostKind, buildHostProcess);
143100
}
144101

145102
return buildHostProcess.BuildHost;
146103
}
147104

105+
async Task<BuildHostProcess> NoLock_GetBuildHostAsync(BuildHostProcessKind buildHostKind, string? projectOrSolutionFilePath, string? dotnetPath, CancellationToken cancellationToken)
106+
{
107+
var pipeName = Guid.NewGuid().ToString();
108+
var processStartInfo = CreateBuildHostStartInfo(buildHostKind, pipeName, dotnetPath);
109+
110+
var process = Process.Start(processStartInfo);
111+
Contract.ThrowIfNull(process, "Process.Start failed to launch a process.");
112+
113+
var buildHostProcess = new BuildHostProcess(process, pipeName, _loggerFactory);
114+
buildHostProcess.Disconnected += BuildHostProcess_Disconnected;
115+
116+
// We've subscribed to Disconnected, but if the process crashed before that point we might have not seen it
117+
if (process.HasExited)
118+
{
119+
buildHostProcess.LogProcessFailure();
120+
throw new Exception($"BuildHost process exited immediately with {process.ExitCode}");
121+
}
122+
123+
if (buildHostKind != BuildHostProcessKind.NetCore
124+
|| projectOrSolutionFilePath is null
125+
|| dotnetPath is not null)
126+
{
127+
return buildHostProcess;
128+
}
129+
130+
// When running on .NET Core, we need to find the right SDK location that can load our project and restart the BuildHost if required.
131+
// When dotnetPath is null, the BuildHost is started with the default dotnet executable, which may not be the right one for the project.
132+
133+
var processPath = GetProcessPath();
134+
135+
// The running BuildHost will be able to search through all the SDK install locations for a usable MSBuild instance.
136+
var msbuildLocation = await buildHostProcess.BuildHost.FindBestMSBuildAsync(projectOrSolutionFilePath, cancellationToken).ConfigureAwait(false);
137+
if (msbuildLocation is null)
138+
{
139+
return buildHostProcess;
140+
}
141+
142+
// The layout of the SDK is such that the dotnet executable is always at the same relative path from the MSBuild location.
143+
dotnetPath = Path.GetFullPath(Path.Combine(msbuildLocation.Path, $"../../{DotnetExecutable}"));
144+
145+
// If the dotnetPath is null or the file doesn't exist, we can't do anything about it; the BuildHost will just use the default dotnet executable.
146+
// If the dotnetPath is the same as processPath then we are already running from the right dotnet executable, so we don't need to relaunch.
147+
if (dotnetPath is null || processPath == dotnetPath || !File.Exists(dotnetPath))
148+
{
149+
return buildHostProcess;
150+
}
151+
152+
// We need to relaunch the .NET BuildHost from a different dotnet instance.
153+
buildHostProcess.Disconnected -= BuildHostProcess_Disconnected;
154+
await buildHostProcess.DisposeAsync().ConfigureAwait(false);
155+
_logger?.LogInformation(".NET BuildHost started from {ProcessPath} reloading to start from {DotnetPath} to match necessary SDK location.", processPath, dotnetPath);
156+
157+
return await NoLock_GetBuildHostAsync(buildHostKind, projectOrSolutionFilePath, dotnetPath, cancellationToken).ConfigureAwait(false);
158+
}
159+
148160
#if NET
149-
static string? GetProcessPath() => Environment.ProcessPath;
161+
static string GetProcessPath() => Environment.ProcessPath ?? throw new InvalidOperationException("Unable to determine the path of the current process.");
150162
#else
151-
static string? GetProcessPath() => Process.GetCurrentProcess().MainModule?.FileName;
163+
static string GetProcessPath() => Process.GetCurrentProcess().MainModule?.FileName ?? throw new InvalidOperationException("Unable to determine the path of the current process.");
152164
#endif
153165
}
154166

0 commit comments

Comments
 (0)