Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run design time builds out-of-process #69616

Merged

Conversation

jasonmalinowski
Copy link
Member

@jasonmalinowski jasonmalinowski commented Aug 18, 2023

This implements out-of-proc design time builds for the LSP language server (i.e. C# extension), which should bring:

  1. Better compatibility with 8.0 SDKs
  2. Better compatibility with non-SDK style projects (i.e. csproj.dll style projects)

This turned out to be relatively straightforward to do: our MSBuildWorkspace code (for the most part) can be split into two pieces:

  1. The bits talking to our MSBuild APIs.
  2. The bits talking to our workspace APIs.

By a bit of luck, when I implemented the LSP server project system I was only calling into the first bits as a way to get command line arguments and document lists, and from there passed that to other systems to create the workspace. And it turned out that the data structures returned from the first part (namely ProjectFileInfo) was almost serializable -- just removing one field and everything could be sent over the wire via StreamJsonRpc. Therefore, this PR splits the stuff that talks to MSBuild (but not the workspace) into a separate executable project named Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost, and makes that bit of code callable via a StreamJsonRpc link over stdout. Then, the language server reimplements the logic VS for Windows uses to pick which project system to use, and launches the right process (.NET Core vs. .NET Framework) and then calls over via StreamJsonRpc.

For the language server, this has some extra impacts on packaging: we now pack the net472 version of the build host in a subdirectory of the main layout so that way we can launch that process. For the .NET Core build host, we just package the additional DLL and run that with dotnet Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll.

Right now this is not adding support for launching processes into any user of MSBuildWorkspace, at least not yet. To keep MSBuildWorkspace working, I'm relying on the (evilish) fact that an .exe (or .dll in the .NET Core case) can still be referenced as 'regular' class library for purposes of loading types from it. So anybody using MSBuildWorkspace via NuGet will now have the BuildHost deployed, but it'll just get loaded as another binary for purposes of in-proc builds. Fixing that means ensuring that the MSBuildWorkspace NuGet package would package both flavors of the build host (regardless of the runtime being consumed) and then from there launch the right one. That's absolutely work I want to do, but I want to land this safely before we start tinkering there since right now the process launching code is making specific assumptions about the layout in the LanguageServer artifacts. There is also some extra layering cleanup that needs to be done: some of the stuff that creates the projects in the workspace itself is a bit more tied to the MSBuild types and that needs to be separated a bit more.

There's also some improvements to be made in the BuildHost process in terms of reducing its disk layout size: right now for the net472 version we'll deploy the C# compiler and workspace binaries...even though we're not really using them. We are also creating a MEF container to check for the existence of some language services but I don't believe they're actually in use -- we could drop MEF entirely and refactor a bit and move some of the "is this language supported?" to the workspace (in-proc) side. I'm deferring those refactorings for now.

Things to Do

  • .NET Framework support.
  • binlog support.
  • Better selection of the right SDK being ran into the host process.
  • Finish NuGet packaging for the existing MSBuildWorkspace packages to ensure I'm not breaking users there.
  • Add an opt-out flag.

Things to Test

  • Test on Mac.
  • Test on Linux.
  • Ensure we correctly report diagnostic logs back if builds still fail.
  • Ensure the new dependency of Microsoft.CodeAnalysis.Workspaces.MSBuild is inserted into VS. (Even though MSBuildWorkspace really isn't intended to be used inside devenv.exe, people absolutely do and we'll break them otherwise.)

Things to Do Later™

This is out of scope for this PR but the intent is still to follow up and do it soon. Since there's enough users who are getting benefit from what we have here, it doesn't necessarily make sense to hold up this PR on this work.

  • Mono support.
  • Switch to a bundled MSBuild for reading solution files, since that's not version specific. This means we can avoid an extra MSBuild discovery step in-proc.
  • Detection of the right .NET Core SDK in the main process and then having more than one .NET Core process for different SDK versions. This is going to be implemented via some support in MSBuildLocator which will have to be consumed here.

Fixes dotnet/vscode-csharp#6126
Fixes dotnet/vscode-csharp#6250
Fixes dotnet/vscode-csharp#6216
Closes dotnet/vscode-csharp#5721

@dotnet-issue-labeler dotnet-issue-labeler bot added Area-IDE untriaged Issues and PRs which have not yet been triaged by a lead labels Aug 18, 2023
@jasonmalinowski jasonmalinowski self-assigned this Aug 18, 2023
@jasonmalinowski jasonmalinowski force-pushed the out-of-proc-design-time-builds branch from 69e1b40 to 151fedf Compare August 22, 2023 21:44
@jasonmalinowski jasonmalinowski force-pushed the out-of-proc-design-time-builds branch 4 times, most recently from 69a69e9 to 8f6d151 Compare August 24, 2023 21:19
@@ -157,7 +156,7 @@ private async Task<bool> TryEnsureMSBuildLoadedAsync(string workingDirectory)
if (msbuildInstance != null)
{
MSBuildLocator.RegisterInstance(msbuildInstance);
_logger.LogInformation($"Loaded MSBuild at {msbuildInstance.MSBuildPath}");
_logger.LogInformation($"Loaded MSBuild in-process from {msbuildInstance.MSBuildPath}");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are still loading an MSBuild version in-process for reading solution files. A bit silly at this point, but I clarified the text to make it less confusing.

@jasonmalinowski jasonmalinowski marked this pull request as ready for review August 25, 2023 00:49
@jasonmalinowski jasonmalinowski requested review from a team as code owners August 25, 2023 00:49
This moves nearly all of the code that uses MSBuild to a separate
project, Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost. This
new project is an executable project, and will eventually be the
separate process that is invoking MSBuild. For now, it's still
consumed as a regular project-to-project reference to keep everything
else working.

There are a few things that are still using MSBuild in the original
library, namely that we have some methods that take an ILogger and
the OpenSolutionAsync method calls into the solution file parsing APIs.
The method that takes an ILogger we will not be able to support in
an out-of-proc mode (although we can still maintain an in-proc compat
mode); the OpenSolutionAsync we will probably be able to address by
having the solution file parsing happening in-process since that
doesn't depend on any project file details.
@jasonmalinowski jasonmalinowski force-pushed the out-of-proc-design-time-builds branch from 9208679 to e9fea77 Compare August 29, 2023 01:35
The shape of this type was a bit deceiving: a ProjectFile has a Log
property, and this also having a Log property makes it appear like
there's one log for the basic loading of the project, and then a second
log for the actual loading for a specific TFM. This is however not the
case: ProjectFileInfo.Log would always be the same instance as the
ProjectFile it came from. Removing this is easy, as the only place
it was ever used was where we already had the ProjectFile in hand,
and it makes ProjectFileInfo a simple data type.
This implements the basic launching of the build host process, setting
up the RPC channel, and running the design-time builds in that process.
Right now this only works for .NET Core projects.
Our rule for project files is two space indent, this was using four.
This is a hack that's inspired from some of the similar things we
do in our interactive window tests where we want to deploy multiple
versions there too.
This implements the selection algorithm, and will launch the .NET
Framework build host if needed.
Besides passing through the option (which we pass as a switch to the
build host process), this also fixes up dispose handling so we properly
shut down the build host. Otherwise we might not have fully flushed the
binary logs in the first place.
This environment variable is set by MSBuildLocator to the version
of MSBuild that is located and registered in a process. We don't want
this to ever be inherited to a build host, since the whole point is
to have a different MSBuild version there.
In the .NET Core case, we still want to consider the project path
to ensure we have the right global.json. Once we can do this detection
in the main process then we can remove this and simplify it again.
@jasonmalinowski jasonmalinowski force-pushed the out-of-proc-design-time-builds branch from f60c2f6 to 4a152e8 Compare August 30, 2023 18:28
Right now our LanguageServer NuGet package is too big to upload, so
this reduces the total size of the package by around 40 MB, but also
simplifies some things and speeds it up too. Previously we were using
a ProjectFileLoaderRegistry to discover the IProjectFileLoader for
a given project; this does a bunch of MEF stuff to find the loader,
allow for custom associations to file types, and also ensures we have
the other workspace binaries for that language.

But IProjectFileLoader isn't public, so at most there could ever be
just two: C# and VB. But we were still MEF composing all of that to
simply discover those two types and also ensure we had the right
workspace binaries deployed to parse the resulting compiler command
line -- even though it's not used in the BuildHost process. But if we
simply skip all of that and just replace it with a switch statement and
a check in-process that we have a ICommandLineParserService, we can
remove the need for MEF entirely, and also stop deploying the C#
binaries altogether, greatly simplifying the BuildHost code.

Although it might not really be necessary long term, I'm moving the
ProjectFileLoaderRegistry and DiagnosticReporter back to the main
MSBuildWorkspace assembly since we don't need it in the BuildHost
either anymore.
This will allow us to also create BuildHost objects in-proc as a
fallback scenario.
private void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
if (e.Data is not null)
LoggerForProcessMessages?.LogTrace($"Message from Process: {e.Data}");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something to consider for later - similar to LSP trace logging, sometimes the json itself is useful to log at the most verbose levels. In case there are weird serialization issues.

@jasonmalinowski jasonmalinowski force-pushed the out-of-proc-design-time-builds branch from c4a411d to b2cc3b8 Compare August 30, 2023 23:38
Trying to ngen this produces some errors because we're not shipping
the dependencies that are used for the console-application portion of it
(like parsing command line switches and locating an MSBuild in that
process). But we don't have any need for that, so we can just disable
ngen to silence errors.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-IDE untriaged Issues and PRs which have not yet been triaged by a lead
Projects
None yet
4 participants