Skip to content

Commit

Permalink
Add support for using Mono to load .NET Framework projects
Browse files Browse the repository at this point in the history
We'll use this on Mac and Linux; we already had equivalent support
on Windows. MonoMSBuildDiscovery.cs is largely a copy of the code
from these paths, with some tweaks to remove Omnisharp scenarios
that don't apply for us.

- https://github.com/OmniSharp/omnisharp-roslyn/blob/dde8119c40f4e3920eb5ea894cbca047033bd9aa/src/OmniSharp.Host/MSBuild/Discovery/Providers/MonoInstanceProvider.cs
- https://github.com/OmniSharp/omnisharp-roslyn/blob/dde8119c40f4e3920eb5ea894cbca047033bd9aa/src/OmniSharp.Host/MSBuild/Discovery/MSBuildInstanceProvider.cs
  • Loading branch information
jasonmalinowski committed Oct 5, 2023
1 parent e7d4a39 commit da9b392
Show file tree
Hide file tree
Showing 5 changed files with 275 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,17 @@ public async Task<IBuildHost> GetBuildHostAsync(string projectFilePath, Cancella

_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;
}

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))
Expand All @@ -58,13 +65,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 +119,7 @@ public async ValueTask DisposeAsync()
await process.DisposeAsync();
}

private Process LaunchDotNetCoreBuildHost()
private ProcessStartInfo CreateDotNetCoreBuildHostStartInfo()
{
var processStartInfo = new ProcessStartInfo()
{
Expand All @@ -125,26 +136,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 +199,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 +230,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
Original file line number Diff line number Diff line change
Expand Up @@ -130,20 +130,13 @@
<!-- We include Build as a target we invoke to work around https://github.com/dotnet/msbuild/issues/5433; we pass
BuildInParallel="false" to avoid multiple builds potentially building the same project at the same time, but since we
expect the builds to have completed by this point they should be fast. -->
<MSBuild Projects="@(_NetFrameworkBuildHostProjectReference)"
Targets="Build;BuiltProjectOutputGroup;ReferenceCopyLocalPathsOutputGroup"
BuildInParallel="false"
Properties="TargetFramework=%(_NetFrameworkBuildHostProjectReference.TargetFramework);RuntimeIdentifier=">
<MSBuild Projects="@(_NetFrameworkBuildHostProjectReference)" Targets="Build;BuiltProjectOutputGroup;ReferenceCopyLocalPathsOutputGroup" BuildInParallel="false" Properties="TargetFramework=%(_NetFrameworkBuildHostProjectReference.TargetFramework);RuntimeIdentifier=">
<Output TaskParameter="TargetOutputs" ItemName="NetFrameworkBuildHostAssets" />
</MSBuild>

<ItemGroup>
<!-- We set Pack="false" because we only care about the RID-specific folders -->
<Content Include="%(NetFrameworkBuildHostAssets.Identity)"
Condition="'%(NetFrameworkBuildHostAssets.TargetPath)' != '' and '%(NetFrameworkBuildHostAssets.Extension)' != '.xml'"
Pack="false"
TargetPath="BuildHost-net472\%(NetFrameworkBuildHostAssets.TargetPath)"
CopyToOutputDirectory="PreserveNewest" />
<Content Include="%(NetFrameworkBuildHostAssets.Identity)" Condition="'%(NetFrameworkBuildHostAssets.TargetPath)' != '' and '%(NetFrameworkBuildHostAssets.Extension)' != '.xml'" Pack="false" TargetPath="BuildHost-net472\%(NetFrameworkBuildHostAssets.TargetPath)" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Target>

Expand Down
71 changes: 52 additions & 19 deletions src/Workspaces/Core/MSBuild.BuildHost/BuildHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,36 +43,61 @@ private bool TryEnsureMSBuildLoaded(string projectOrSolutionFilePath)
return true;
}

VisualStudioInstance? instance;
if (!PlatformInformation.IsRunningOnMono)
{

VisualStudioInstance? instance;

#if NETFRAMEWORK

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

// TODO: we should also check that the managed tools are actually installed
instance = MSBuildLocator.QueryVisualStudioInstances().OrderByDescending(vs => vs.Version).FirstOrDefault();
// TODO: we should also check that the managed tools are actually installed
instance = MSBuildLocator.QueryVisualStudioInstances().OrderByDescending(vs => vs.Version).FirstOrDefault();

#else

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

#endif

if (instance != null)
{
MSBuildLocator.RegisterInstance(instance);
_logger.LogInformation($"Registered MSBuild instance at {instance.MSBuildPath}");
return true;
if (instance != null)
{
MSBuildLocator.RegisterInstance(instance);
_logger.LogInformation($"Registered MSBuild instance at {instance.MSBuildPath}");
}
else
{
_logger.LogCritical("No compatible MSBuild instance could be found.");
}
}
else
{
_logger.LogCritical("No compatible MSBuild instance could be found.");
return false;
#if NETFRAMEWORK

// 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.
var monoMSBuildDirectory = MonoMSBuildDiscovery.GetMonoMSBuildDirectory();

if (monoMSBuildDirectory != null)
{
MSBuildLocator.RegisterMSBuildPath(monoMSBuildDirectory);
_logger.LogInformation($"Registered MSBuild instance at {monoMSBuildDirectory}");
}
else
{
_logger.LogCritical("No Mono MSBuild installation could be found; see https://www.mono-project.com/ for installation instructions.");
}

#else
_logger.LogCritical("Trying to run the .NET Core BuildHost on Mono is unsupported.");
#endif
}

return MSBuildLocator.IsRegistered;
}
}

Expand Down Expand Up @@ -117,9 +142,17 @@ private void EnsureMSBuildLoaded(string projectFilePath)
[MethodImpl(MethodImplOptions.NoInlining)] // Do not inline this, since this uses MSBuild types which are being loaded by the caller
private static ImmutableArray<(string ProjectPath, string ProjectGuid)> GetProjectsInSolution(string solutionFilePath)
{
return SolutionFile.Parse(solutionFilePath).ProjectsInOrder
.Where(static p => p.ProjectType != SolutionProjectType.SolutionFolder)
.SelectAsArray(static p => (p.AbsolutePath, p.ProjectGuid));
var builder = ImmutableArray.CreateBuilder<(string ProjectPath, string ProjectGuid)>();

foreach (var project in SolutionFile.Parse(solutionFilePath).ProjectsInOrder)
{
if (project.ProjectType != SolutionProjectType.SolutionFolder)
{
builder.Add((project.AbsolutePath, project.ProjectGuid));
}
}

return builder.ToImmutable();
}

public Task<bool> IsProjectFileSupportedAsync(string projectFilePath, CancellationToken cancellationToken)
Expand Down
Loading

0 comments on commit da9b392

Please sign in to comment.