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

Re-architect how MSBuild is loaded by OmniSharp #988

Merged
merged 17 commits into from
Oct 27, 2017

Conversation

DustinCampbell
Copy link
Contributor

@DustinCampbell DustinCampbell commented Oct 25, 2017

With MSBuild 15, locating MSBuild on the user's box can be a significant challenge. OmniSharp provides a version of MSBuild as a fallback, but that can often only work with .NET Core/Standard SDK-style projects; projects targeting .NET Framework can run into significant issues.

This PR introduces an IMSBuildLocator service that is used to discover instances of MSBuild 15 installed on the user's machine. It can discover MSBuild 15 using a few methods:

  1. (Windows only) Using the COM-based Visual Studio setup API.
  2. (Windows only) Checking environment variables that are set by a Visual Studio Developer Prompt
  3. (OSX/Linux only) Looking for a global Mono installation that contains MSBuild 15.
  4. If MSBuild 15 can't be discovered using the three methods above, it falls back to the standalone MSBuild included in OmniSharp.

Once MSBuild is located, OmniSharp will install an AppDomain.AssemblyResolve event to ensure that it loads the Microsoft.Build.* assemblies from that location. This ensures that the MSBuild used by OmniSharp will be compatible with whatever MSBuild toolset is used to process the user's project. In addition, this can speed up processing depending on whether the Microsoft.Build.* assemblies are NGen'd or AOT'd in the location they're loaded from.

Note that, because we're using AssemblyResolve to load Microsoft.Build.* assemblies, the event must be hooked before OmniSharp is composed with MEF. Otherwise, MEF will fail to compose.

In addition, as part of this change, I've reorganized the .msbuild folder to layout more closely to how it's laid out in Visual Studio.

A few things I want to do before merging this:

  • Delete the MSBuildEnvironment class. This is still there because there's still logic in this class that hasn't been replicated in an MSBuild discoverer yet.
  • More testing on OSX/Linux with and without Mono installed.
  • Testing on OSX/Linux when an older Mono is installed.
  • Add more logging in MSBuild discovery to help identify failures (especially with Mono).

Final note: There's an issue with Mono and AssemblyResolve where it can get into a bad state an attempt is made to load an assembly before the AssemblyResolve event is hooked. On desktop CLR, this works as expected. The core issue here is that xUnit uses Assembly.Load on every assembly in the directory to try and locate xUnit types and that happens before our AssemblyResolve is installed. Currently, this is working OK on OSX/Linux, but it's going to be a problem at some point. I've filed the following bug on Mono to get this fixed: https://bugzilla.xamarin.com/show_bug.cgi?id=60100.

.travis.yml Outdated
if [ "$TRAVIS_OS_NAME" == "osx" ]; then
rvm install ruby-2.3.3
fi

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is old code. Will remove this.

var msbuildLocator = _serviceProvider.GetRequiredService<IMSBuildLocator>();
var instances = msbuildLocator.GetInstances();
var instance = instances.FirstOrDefault();
if (instance != null)
Copy link

Choose a reason for hiding this comment

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

Are we supposed to do anything if we couldn't locate any instances?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We should probably report an error to the log. Will add that.

}
}

return null;
Copy link

Choose a reason for hiding this comment

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

For my own edification, what happens if Resolve returns null?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The runtime will continue to the next AssemblyResolve handler it finds. From https://msdn.microsoft.com/en-us/library/system.appdomain.assemblyresolve%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396:

If more than one event handler is registered for this event, the event handlers are called in order until an event handler returns a value that isn't null. Subsequent event handlers are ignored.

return msbuildDirectory;
}

private static string FindLocalMSBuildFromSolution()
Copy link

Choose a reason for hiding this comment

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

What about adding a command line argument that we can pass while debugging that specifies the standalone msbuild location? That way we don't have to talk about "OmniSharp.sln" in the product code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe, but where would it get populated from? Note that the product code already referenced "OmniSharp.sln". This PR just moved that code to here.

Copy link

Choose a reason for hiding this comment

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

We would add the argument to the arguments passed by the debugger or by tests. Not a huge deal either way.

Copy link
Contributor Author

@DustinCampbell DustinCampbell Oct 26, 2017

Choose a reason for hiding this comment

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

I suppose we could add some MSBuild logic to add an argument passed to the debugger (which still worked when other arguments were added). I'm not sure how it'd work for tests, but I'm guessing we could figure something out. I'm just not sure it's all that valuable.

var instances = new ISetupInstance[1];
do
{
instanceEnum.Next(1, instances, out fetched);
Copy link

Choose a reason for hiding this comment

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

Does this only enumerate Dev15 versions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's all it can do. This is the API for the Willow installer.

Copy link
Member

Choose a reason for hiding this comment

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

Which gets picked if someone has dev15 and a dev15 preview installed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a bit arbitrary. We don't have a way for the user to select one, so it's whichever is first in the list. However, we check for the Visual Studio tied to a developer command prompt first. So, if the user wants to launch OmniSharp with a specific Visual Studio they can launch it from the command prompt that was installed for that VS. Alternatively, they can set the environment variables that the developer command prompt uses.


return ImmutableArray.Create(
new MSBuildInstance(
"DEVCONSOLE",
Copy link

Choose a reason for hiding this comment

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

nameof(DiscoveryType.DevConsole)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea

This change re-organizes the layout of the MSBuild folder to more closely
match the layout of Visual Studio:

* "msbuild" - This will be the MSBuildExtensionsPath
* "msbuild\15.0" - common props go here
* "msbuild\15.0\bin" - The MSBuild tools path, where much of the MSBuild actually goes.

In addition, the MSBuild runtime and library packages have been updated to the latest,
and the package references for the libraries are marked as `PrivateAssets="all"` in the
OmniSharp.MSBuild project. This last change means that OmniSharp no longer runs, since the
MSBuild libraries aren't copied to the OmniSharp base application path any longer. Later
commits will fix this.
The MSBuildLocator service has provider model for discovering MSBuild in various scenarios:

* DevConsole
* VS 2017 installation
* Mono
* Stand alone (i.e. the MSBuild that we ship with OmniSharp)

Once an MSBuild instance is discovered and registered, an AssemblyResolve event is used to
ensure that various MSBuild assemblies are used from that instance rather than using MSBuild
assemblies included with OmniSharp.
@DustinCampbell
Copy link
Contributor Author

@david-driscoll, @filipw, @rchande : I've done a fair amount of manual testing with this change on OSX and am feeling pretty confident about it. The core MSBuild project scenarios are still working, some scenarios work better, and some new scenarios have starting working. Here are the scenarios I tested:

  • No mono installed + run OmniSharp standalone:
    • Omnisharp-roslyn does not work (expected due to .NETFramework, 4.6 being missing)
    • .NET Core MVC project works with .NET Core SDK installed
    • Unity does not work (expected due to .NETFramework 2.0 being missing)
  • Mono 4.8 installed + run OmniSharp standalone:
    • Omnisharp-roslyn works
    • .NET Core MVC project works
    • Unity project works
  • Mono 5.2 installed + run OmniSharp standalone:
  • Mono 5.2 installed + run OmniSharp on installed Mono:
  • Mono 5.4 installed + run OmniSharp standalone:
  • Mono 5.4 installed + run OmniSharp on installed Mono:

{
var assemblyPath = Path.Combine(_registeredInstance.MSBuildPath, assemblyName.Name + ".dll");
var result = File.Exists(assemblyPath)
? Assembly.LoadFrom(assemblyPath)
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, you're right. I didn't use it initially because I was still trying to figure out when/how the IMSBuildLocator would be initialized. New commit adds this.

@filipw
Copy link
Member

filipw commented Oct 27, 2017

Looks like a massive improvement 👍👍👍

One question, should this also recognize that I have just the standalone MsBuild Tools installed, even if I don't have VS installed?
Maybe it's edge scenario, but the reason I mention that is that this would be the set up on Build Agents (where no VS is installed). Personally, I'd often run VS Code + C# extension on those machines, in case some failing builds need debugging or closer look.

@DustinCampbell
Copy link
Contributor Author

@filipw: I haven't specifically tested that scenario, but it should already work. The MSBuild 15.0 build tools use the same installer as VS 2017. So, the MSBuild discovery that looks for Visual Studio using the setup API should also work for the Build Tools as well.

var instances = new ISetupInstance[1];
do
{
instanceEnum.Next(1, instances, out fetched);
Copy link
Member

Choose a reason for hiding this comment

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

Which gets picked if someone has dev15 and a dev15 preview installed?

@@ -20,6 +21,32 @@ public override ImmutableArray<MSBuildInstance> GetInstances()
return NoInstances;
}

// Don't resolve to MSBuild assemblies under the installed Mono unless OmniSharp
// is actually running under the installed Mono.
var monoRuntimePath = PlatformHelper.GetMonoRuntimePath();
Copy link

Choose a reason for hiding this comment

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

Is this obvious? Maybe explain a bit further what goes wrong if this is not the case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

None of this is obvious. 😄 But you're right, I'll add a little bit more detail about how it can go wrong.

@rchande
Copy link

rchande commented Oct 27, 2017

Thanks for testing this so thoroughly and documenting what you tested @DustinCampbell. cc @piotrpMSFT #988 (comment) explains the size of the test matrix.

@DustinCampbell
Copy link
Contributor Author

FWIW, I don't normally test all of those scenarios, but they were necessary for this particular change. Too often, changes like these have broken unity support.

@DustinCampbell DustinCampbell self-assigned this Oct 27, 2017
@DustinCampbell
Copy link
Contributor Author

OK. I think it's good now. I'm going to go ahead and merge. Let me know if you think of anything else.

@DustinCampbell DustinCampbell merged commit f0fc64e into OmniSharp:master Oct 27, 2017
@filipw
Copy link
Member

filipw commented Oct 27, 2017

excellent!

@enricosada
Copy link

enricosada commented Oct 27, 2017

awesome. i am going to steal it. thx @DustinCampbell !
in f# land we just really need the location of msbuild.exe and is tricky to find

@jetersen
Copy link

@DustinCampbell Did you try mono mdk? the package provided by Xamarin people through brew? 👍

@DustinCampbell
Copy link
Contributor Author

I installed the Mono MDK with the installer

@jetersen
Copy link

👍 Awesome

@DustinCampbell
Copy link
Contributor Author

FWIW, I'll check brew install cask mono-mdk on Monday.

@jetersen
Copy link

SAME 👍

@corngood
Copy link
Contributor

This seems to have broken mono/windows, see #1001

@DustinCampbell
Copy link
Contributor Author

@corngood: Why are you running OmniSharp on Mono/Windows? We've never tested that scenario. The assumption has generally been that desktop CLR on Windows is the way to go for OmniSharp.

@DustinCampbell
Copy link
Contributor Author

@Casz: I just checked brew cask install mono-mdk and it works fine.

@corngood
Copy link
Contributor

@DustinCampbell I'm working around a bug w/ net46. We can discuss on #1001

@jetersen
Copy link

@DustinCampbell I tested it with our dotnet 4.5 project and it did not work 😢

@DustinCampbell
Copy link
Contributor Author

@Casz: Bummer! Could you file an issue with your OmniSharp log?

@jetersen
Copy link

@DustinCampbell did I have to do something special or was downloading c# extension enough for VS Code?
Using 1.12.1 c# extension in vscode

@jetersen
Copy link

Oh found the beta install guide... my bad

@jetersen
Copy link

@DustinCampbell it works almost dotnet/vscode-csharp#1821

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants