Skip to content

Commit

Permalink
Merge pull request #70263 from jasonmalinowski/add-mono-support
Browse files Browse the repository at this point in the history
Add support for using Mono to load .NET Framework projects
  • Loading branch information
jasonmalinowski authored Oct 12, 2023
2 parents b837e9d + 80e707d commit 07f809a
Show file tree
Hide file tree
Showing 18 changed files with 430 additions and 189 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,27 +29,39 @@ public BuildHostProcessManager(ILoggerFactory? loggerFactory = null, string? bin
_binaryLogPath = binaryLogPath;
}

public async Task<IBuildHost> GetBuildHostAsync(string projectFilePath, CancellationToken cancellationToken)
/// <summary>
/// Returns the best <see cref="IBuildHost"/> to use for this project; if it picked a fallback option because the preferred kind was unavailable, that's returned too, otherwise null.
/// </summary>
public async Task<(IBuildHost, BuildHostProcessKind? PreferredKind)> GetBuildHostAsync(string projectFilePath, CancellationToken cancellationToken)
{
var neededBuildHostKind = GetKindForProject(projectFilePath);
BuildHostProcessKind? preferredKind = null;

_logger?.LogTrace($"Choosing a build host of type {neededBuildHostKind} for {projectFilePath}.");

if (neededBuildHostKind == BuildHostProcessKind.Mono && MonoMSBuildDiscovery.GetMonoMSBuildDirectory() == null)
{
_logger?.LogWarning($"An installation of Mono could not be found; {projectFilePath} will be loaded with the .NET Core SDK and may encounter errors.");
neededBuildHostKind = BuildHostProcessKind.NetCore;
preferredKind = BuildHostProcessKind.Mono;
}

var buildHost = await GetBuildHostAsync(neededBuildHostKind, cancellationToken).ConfigureAwait(false);

// 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.
// Check if this is the case.
// Check if this is the case. Unlike the mono case, we have to actually ask the other process since MSBuildLocator only allows
// us to discover VS instances in .NET Framework hosts right now.
if (neededBuildHostKind == BuildHostProcessKind.NetFramework)
{
if (!await buildHost.HasUsableMSBuildAsync(projectFilePath, cancellationToken))
{
// It's not usable, so we'll fall back to the .NET Core one.
_logger?.LogWarning($"An installation of Visual Studio or the Build Tools for Visual Studio could not be found; {projectFilePath} will be loaded with the .NET Core SDK and may encounter errors.");
return await GetBuildHostAsync(BuildHostProcessKind.NetCore, cancellationToken);
return (await GetBuildHostAsync(BuildHostProcessKind.NetCore, cancellationToken), PreferredKind: BuildHostProcessKind.NetFramework);
}
}

return buildHost;
return (buildHost, preferredKind);
}

public async Task<IBuildHost> GetBuildHostAsync(BuildHostProcessKind buildHostKind, CancellationToken cancellationToken)
Expand All @@ -58,13 +70,17 @@ public async Task<IBuildHost> GetBuildHostAsync(BuildHostProcessKind buildHostKi
{
if (!_processes.TryGetValue(buildHostKind, out var buildHostProcess))
{
var process = buildHostKind switch
var processStartInfo = buildHostKind switch
{
BuildHostProcessKind.NetCore => LaunchDotNetCoreBuildHost(),
BuildHostProcessKind.NetFramework => LaunchDotNetFrameworkBuildHost(),
BuildHostProcessKind.NetCore => CreateDotNetCoreBuildHostStartInfo(),
BuildHostProcessKind.NetFramework => CreateDotNetFrameworkBuildHostStartInfo(),
BuildHostProcessKind.Mono => CreateMonoBuildHostStartInfo(),
_ => throw ExceptionUtilities.UnexpectedValue(buildHostKind)
};

var process = Process.Start(processStartInfo);
Contract.ThrowIfNull(process, "Process.Start failed to launch a process.");

buildHostProcess = new BuildHostProcess(process, _loggerFactory);
buildHostProcess.Disconnected += BuildHostProcess_Disconnected;
_processes.Add(buildHostKind, buildHostProcess);
Expand Down Expand Up @@ -108,7 +124,7 @@ public async ValueTask DisposeAsync()
await process.DisposeAsync();
}

private Process LaunchDotNetCoreBuildHost()
private ProcessStartInfo CreateDotNetCoreBuildHostStartInfo()
{
var processStartInfo = new ProcessStartInfo()
{
Expand All @@ -125,26 +141,41 @@ private Process LaunchDotNetCoreBuildHost()

AppendBuildHostCommandLineArgumentsConfigureProcess(processStartInfo);

var process = Process.Start(processStartInfo);
Contract.ThrowIfNull(process, "Process.Start failed to launch a process.");
return process;
return processStartInfo;
}

private Process LaunchDotNetFrameworkBuildHost()
private ProcessStartInfo CreateDotNetFrameworkBuildHostStartInfo()
{
var netFrameworkBuildHost = Path.Combine(Path.GetDirectoryName(typeof(BuildHostProcessManager).Assembly.Location)!, "BuildHost-net472", "Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.exe");
Contract.ThrowIfFalse(File.Exists(netFrameworkBuildHost), $"Unable to locate the .NET Framework build host at {netFrameworkBuildHost}");

var netFrameworkBuildHost = GetPathToDotNetFrameworkBuildHost();
var processStartInfo = new ProcessStartInfo()
{
FileName = netFrameworkBuildHost,
};

AppendBuildHostCommandLineArgumentsConfigureProcess(processStartInfo);

var process = Process.Start(processStartInfo);
Contract.ThrowIfNull(process, "Process.Start failed to launch a process.");
return process;
return processStartInfo;
}

private ProcessStartInfo CreateMonoBuildHostStartInfo()
{
var processStartInfo = new ProcessStartInfo
{
FileName = "mono"
};

processStartInfo.ArgumentList.Add(GetPathToDotNetFrameworkBuildHost());

AppendBuildHostCommandLineArgumentsConfigureProcess(processStartInfo);

return processStartInfo;
}

private static string GetPathToDotNetFrameworkBuildHost()
{
var netFrameworkBuildHost = Path.Combine(Path.GetDirectoryName(typeof(BuildHostProcessManager).Assembly.Location)!, "BuildHost-net472", "Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.exe");
Contract.ThrowIfFalse(File.Exists(netFrameworkBuildHost), $"Unable to locate the .NET Framework build host at {netFrameworkBuildHost}");
return netFrameworkBuildHost;
}

private void AppendBuildHostCommandLineArgumentsConfigureProcess(ProcessStartInfo processStartInfo)
Expand Down Expand Up @@ -173,10 +204,6 @@ private void AppendBuildHostCommandLineArgumentsConfigureProcess(ProcessStartInf

private static BuildHostProcessKind GetKindForProject(string projectFilePath)
{
// At the moment we don't have mono support here, so if we're not on Windows, we'll always force to .NET Core.
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return BuildHostProcessKind.NetCore;

// This implements the algorithm as stated in https://github.com/dotnet/project-system/blob/9a761848e0f330a45e349685a266fea00ac3d9c5/docs/opening-with-new-project-system.md;
// we'll load the XML of the project directly, and inspect for certain elements.
XDocument document;
Expand Down Expand Up @@ -208,13 +235,14 @@ private static BuildHostProcessKind GetKindForProject(string projectFilePath)
return BuildHostProcessKind.NetCore;

// Nothing that indicates it's an SDK-style project, so use our .NET framework host
return BuildHostProcessKind.NetFramework;
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? BuildHostProcessKind.NetFramework : BuildHostProcessKind.Mono;
}

public enum BuildHostProcessKind
{
NetCore,
NetFramework
NetFramework,
Mono
}

private sealed class BuildHostProcess : IAsyncDisposable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
Expand All @@ -18,6 +19,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.Composition;
using Roslyn.Utilities;
using static Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.BuildHostProcessManager;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
Expand Down Expand Up @@ -96,7 +98,10 @@ public async Task OpenSolutionAsync(string solutionFilePath)

// If we don't have a .NET Core SDK on this machine at all, try .NET Framework
if (!await buildHost.HasUsableMSBuildAsync(solutionFilePath, CancellationToken.None))
buildHost = await buildHostProcessManager.GetBuildHostAsync(BuildHostProcessManager.BuildHostProcessKind.NetFramework, CancellationToken.None);
{
var kind = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? BuildHostProcessKind.NetFramework : BuildHostProcessKind.Mono;
buildHost = await buildHostProcessManager.GetBuildHostAsync(kind, CancellationToken.None);
}

foreach (var project in await buildHost.GetProjectsInSolutionAsync(solutionFilePath, CancellationToken.None))
{
Expand Down Expand Up @@ -144,14 +149,22 @@ private async ValueTask LoadOrReloadProjectsAsync(ImmutableSegmentedList<Project
{
tasks.Add(Task.Run(async () =>
{
var errorKind = await LoadOrReloadProjectAsync(projectToLoad, buildHostProcessManager, cancellationToken);
var (errorKind, preferredBuildHostKind) = await LoadOrReloadProjectAsync(projectToLoad, buildHostProcessManager, cancellationToken);
if (errorKind is LSP.MessageType.Error)
{
// We should display a toast when the value of displayedToast is 0. This will also update the value to 1 meaning we won't send any more toasts.
var shouldShowToast = Interlocked.CompareExchange(ref displayedToast, value: 1, comparand: 0) == 0;
if (shouldShowToast)
{
var message = string.Format(LanguageServerResources.There_were_problems_loading_project_0_See_log_for_details, Path.GetFileName(projectToLoad.Path));
string message;
if (preferredBuildHostKind == BuildHostProcessKind.NetFramework)
message = LanguageServerResources.Projects_failed_to_load_because_MSBuild_could_not_be_found;
else if (preferredBuildHostKind == BuildHostProcessKind.Mono)
message = LanguageServerResources.Projects_failed_to_load_because_Mono_could_not_be_found;
else
message = string.Format(LanguageServerResources.There_were_problems_loading_project_0_See_log_for_details, Path.GetFileName(projectToLoad.Path));
await ShowToastNotification.ShowToastNotificationAsync(errorKind.Value, message, cancellationToken, ShowToastNotification.ShowCSharpLogsCommand);
}
}
Expand Down Expand Up @@ -179,13 +192,15 @@ private async ValueTask LoadOrReloadProjectsAsync(ImmutableSegmentedList<Project
return binaryLogPath;
}

private async Task<LSP.MessageType?> LoadOrReloadProjectAsync(ProjectToLoad projectToLoad, BuildHostProcessManager buildHostProcessManager, CancellationToken cancellationToken)
private async Task<(LSP.MessageType? failureType, BuildHostProcessKind? preferredKind)> LoadOrReloadProjectAsync(ProjectToLoad projectToLoad, BuildHostProcessManager buildHostProcessManager, CancellationToken cancellationToken)
{
BuildHostProcessKind? preferredBuildHostKind = null;

try
{
var projectPath = projectToLoad.Path;

var buildHost = await buildHostProcessManager!.GetBuildHostAsync(projectPath, cancellationToken);
(var buildHost, preferredBuildHostKind) = await buildHostProcessManager!.GetBuildHostAsync(projectPath, cancellationToken);

if (await buildHost.IsProjectFileSupportedAsync(projectPath, cancellationToken))
{
Expand All @@ -197,7 +212,7 @@ private async ValueTask LoadOrReloadProjectsAsync(ImmutableSegmentedList<Project
var projectLanguage = loadedProjectInfos.FirstOrDefault()?.Language;
if (projectLanguage != null && _workspaceFactory.Workspace.Services.GetLanguageService<ICommandLineParserService>(projectLanguage) == null)
{
return null;
return (null, null);
}

var existingProjects = _loadedProjects.GetOrAdd(projectPath, static _ => new List<LoadedProject>());
Expand Down Expand Up @@ -242,21 +257,21 @@ private async ValueTask LoadOrReloadProjectsAsync(ImmutableSegmentedList<Project
_logger.Log(logItem.Kind is WorkspaceDiagnosticKind.Failure ? LogLevel.Error : LogLevel.Warning, $"{logItem.Kind} while loading {logItem.ProjectFilePath}: {logItem.Message}");
}

return diagnosticLogItems.Any(logItem => logItem.Kind is WorkspaceDiagnosticKind.Failure) ? LSP.MessageType.Error : LSP.MessageType.Warning;
return (diagnosticLogItems.Any(logItem => logItem.Kind is WorkspaceDiagnosticKind.Failure) ? LSP.MessageType.Error : LSP.MessageType.Warning, preferredBuildHostKind);
}
else
{
_logger.LogInformation($"Successfully completed load of {projectPath}");
return null;
return (null, null);
}
}

return null;
return (null, null);
}
catch (Exception e)
{
_logger.LogError(e, $"Exception thrown while loading {projectToLoad.Path}");
return LSP.MessageType.Error;
return (LSP.MessageType.Error, preferredBuildHostKind);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,12 @@
<data name="Passed" xml:space="preserve">
<value>Passed!</value>
</data>
<data name="Projects_failed_to_load_because_Mono_could_not_be_found" xml:space="preserve">
<value>Projects failed to load because the Mono could not be found. Ensure that Mono and MSBuild are installed, and check the logs for details.</value>
</data>
<data name="Projects_failed_to_load_because_MSBuild_could_not_be_found" xml:space="preserve">
<value>Projects failed to load because the .NET Framework build tools could not be found. Try installing Visual Studio or the Visual Studio Build Tools package, or check the logs for details.</value>
</data>
<data name="Running_tests" xml:space="preserve">
<value>Running tests...</value>
</data>
Expand Down Expand Up @@ -192,12 +198,6 @@
<data name="There_were_problems_loading_project_0_See_log_for_details" xml:space="preserve">
<value>There were problems loading project {0}. See log for details.</value>
</data>
<data name="There_were_problems_loading_solution_0_See_log_for_details" xml:space="preserve">
<value>There were errors loading solution {0}. See log for details.</value>
</data>
<data name="There_were_problems_loading_your_projects_See_log_for_details" xml:space="preserve">
<value>There were problems loading your projects. See log for details.</value>
</data>
<data name="Using_runsettings_file_at_0" xml:space="preserve">
<value>Using .runsettings file at {0}</value>
</data>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 07f809a

Please sign in to comment.