dotnet / msbuild

The Microsoft Build Engine (MSBuild) is the build platform for .NET and Visual Studio.
https://docs.microsoft.com/visualstudio/msbuild/msbuild
MIT License
5.23k stars 1.35k forks source link

Cannot use MSBuild API from a dotnet-CLI project tool #1097

Closed natemcmaster closed 6 years ago

natemcmaster commented 8 years ago

Using MSBuild API for in-proj project evaluation in a dotnet-CLI project tool throws this excpetion:

Unhandled Exception: System.TypeInitializationException: The type initializer for 'BuildEnvironmentHelperSingleton' threw an exception. ---> System.InvalidOperationException: Could not determine a valid location to MSBuild. Try running this process from the Developer Command Prompt for Visual Studio.
   at Microsoft.Build.Shared.ErrorUtilities.ThrowInvalidOperation(String resourceName, Object[] args)
   at Microsoft.Build.Shared.BuildEnvironmentHelper.Initialize()
   at Microsoft.Build.Shared.BuildEnvironmentHelper.BuildEnvironmentHelperSingleton..cctor()

IIUC the problem is that BuildEnvironmentHelperSingleton looks in AppContext.BaseDirectory (and a few other locations) for MSBuild.exe. When dotnet-CLI invokes a project tool, the AppContext.BaseDirectory will be $(NuGetPackages)/.tools/$(ToolName)/$(ToolVersion)/netcoreapp1.0/. The only content NuGet/CLI will put into this directory is the *.deps.json file and project.lock.json file for the tool.

Using Microsoft.Build.Runtime 15.1.262-preview5.

cc @piotrpMSFT

jeffkl commented 8 years ago

I'll need to know more about the layout of these project tools. Who is the best person to give me a primer in order to come up with a fix?

cc @eerhardt

NTaylorMullen commented 8 years ago

I'll ping you offline.

rainersigwald commented 8 years ago

I see two options:

  1. Use the CLI's MSBuild toolset (this isn't currently easy to find, on purpose)
  2. Distribute an MSBuild toolset with the tool.

The latter is how standalone applications would work. What's creating this $(NuGetPackages)/.tools layout, and why doesn't it match what would happen with a published application?

natemcmaster commented 8 years ago

What's creating this $(NuGetPackages)/.tools layout, and why doesn't it match what would happen with a published application?

NuGet creates the .tools folder during restore. CLI reads the csproj/.tools folder to launch the tool. AFAIK there aren't any plans to change this. cref https://github.com/NuGet/Home/issues/3462. cc @emgarten @eerhardt

Regardless of how a CLI tools finds or distributes MSBuild, we need some sort of API so that BuildEnvironmentHelper.Initialize knows where to find MSBuild.exe.

eerhardt commented 8 years ago

@piotrpMSFT - do you have any opinions on option 1 above?

@jeffkl - we've discussed before enabling MSBuild to "run out of a NuGet cache", in order for every tool to not need its own private copy of MSBuild. Has any progress been made in this area?

rainersigwald commented 8 years ago

@eerhardt The blockers for making MSBuild run out of the NuGet cache remain: either smarter loading behavior (load our assemblies out of a folder whose layout we totally control) or a full-featured "find files from the nuget cache" feature baked into the framework. Neither seems forthcoming.

natemcmaster commented 8 years ago

There are possible changes coming soon to NuGet/CLI which may solve this issue independently of changes to MSBuild.

cc @yishaigalatzer @rrelyea - the discussion we had about location of a CLI tool's .deps.json file.

eerhardt commented 8 years ago

In our meeting this morning, we discussed this and the answer was:

The CLI will set an environment variable when invoking a tool that will point to the location of it's MSBuild install. The CLI Tool can read this environment variable to find out the path to MSBuild's installation.

mishra14 commented 8 years ago

🔔 Any update on this? Running into the same issue.

natemcmaster commented 8 years ago

In preview3, the SDK will set the env variable MSBUILD_EXE_PATH to the location of MSBuild.dll.

FWIW, in most cases, CLI tools are better off invoking "dotnet-msbuild" with a custom target instead of invoking MSBuild APIs directly.

mishra14 commented 7 years ago

Got it to work, thanks to @rainersigwald :)

In powershell -

  1. $ENV:MSBUILD_EXE_PATH="C:\Program Files\dotnet\sdk\1.0.0-preview4-004079\MSBuild.dll"
  2. $ENV:MSBUILDEXTENSIONSPATH="C:\Program Files\dotnet\sdk\1.0.0-preview4-004079"

Here C:\Program Files\dotnet\sdk\1.0.0-preview4-004079 is the install path for dotnet sdk on my machine

TheRealPiotrP commented 7 years ago

@mishra14 this is very fragile. The code makes assumptions on where CLI installs MSBuild.dll which are quite certain to be broken in upcoming releases. As @natemcmaster says, the CLI already has an environment variable which it passes to processes it creates to identify the msbuild exe path. Even this should only be used in critical scenarios where invoking dotnet msbuild simply won't work.

I know the self-created environment variable you suggest is straightforward and works at the moment, but do expect it to fail from release to release.

mishra14 commented 7 years ago

@piotrpMSFT : @rainersigwald mentioned that the workaround $ENV:MSBUILDEXTENSIONSPATH="C:\Program Files\dotnet\sdk\1.0.0-preview4-004079" is needed due to a bug in current msbuild release but should be fixed in the next release (rc2?).

$ENV:MSBUILD_EXE_PATH="C:\Program Files\dotnet\sdk\1.0.0-preview4-004079\MSBuild.dll" on the otherhand should be set by dotnet sdk. But did not on my machine. Maybe, I am missing something?

TheRealPiotrP commented 7 years ago

It's a subtlety. Dotnet SDK does not set any persistent environment variables. It only passes some environment variables to processes that it itself creates. Tools that need access to this value from the CLI need to be invoked by the CLI, giving the product the opportunity to redefine its own internal layout/implementation without breaking existing extensions :)

rainersigwald commented 7 years ago

@piotrpMSFT It's worse than that. When you're developing the tool, you can't rely on the CLI setting that variable, because it's not set in dotnet run invocations, just tool invocations. Once the tool is packaged, referred to as a tool, and run through the CLI, it should get the CLI's preferred MSBUILD_EXE_PATH. But is there an easy way to edit-compile-debug in that environment?

(the need for MSBUILDEXTENSIONSPATH is fixed by #1336)

jeffkl commented 7 years ago

@mishra14 using dotnet run should work if your app is a netcoreapp1.0. Are you getting error when running your app via dotnet run or when you run it as a tool?

Are you getting the error that MSBuild cannot find itself?

System.InvalidOperationException: Could not determine a valid location to MSBuild. Try running this process from the Developer Command Prompt for Visual Studio.

Or are you getting a different error?

The dotnet run scenario should work because we look in the AppContext.BaseDirectory and should be able to locate assembly dependencies via NuGet. However, as a tool, the AppContext.BaseDirectory is where the tool marker was written to and is not correct. But I believe the dotnet CLI will set MSBUILD_EXE_PATH when the tool is run which means it should work too. I need more info on what error you're getting...

mishra14 commented 7 years ago

@jeffkl : I have dotnet core sdk preview4 installed.

PS C:\Users\anmishr\Documents\visual studio 2017\Projects\ConsoleApp1\ConsoleApp1> dotnet --info
.NET Command Line Tools (1.0.0-preview4-004079)

Product Information:
 Version:            1.0.0-preview4-004079
 Commit SHA-1 hash:  43dfa6b8ba

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.14393
 OS Platform: Windows
 RID:         win10-x64

Are you getting the error that MSBuild cannot find itself? Yes

TheRealPiotrP commented 7 years ago

@livarcocc can comment on where we expose the env var. If adding it to run prevents folks from assuming things about the CLI install layout, maybe we need to do that...

jeffkl commented 7 years ago

@mishra14 are you getting the error when you do dotnet run? Can you check to see if files like MSBuild.dll, Microsoft.Common.targets are in your output folder.

livarcocc commented 7 years ago

We only expose it in the ProjectToolsCommandResolver and ProjectDependenciesCommandResolver right now. I think exposing it during run may make sense for people developing tools.

mishra14 commented 7 years ago

@jeffkl :

are you getting the error when you do dotnet run?: Yes on dotnet run and also from within dev15.

Can you check to see if files like MSBuild.dll, Microsoft.Common.targets are in your output folder: No. I thinks that why I needed to set the env vars. So that dotnet could pick up the correcxt place where msbuild exists.

jeffkl commented 7 years ago

@mishra14 What packages are you referencing? The Microsoft.Build.Runtime package should place stuff in your output folder so that dotnet run works. If those files aren't there, this might be a bug in NuGet.

mishra14 commented 7 years ago

@jeffkl : I do have runtime.

<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp1.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <Compile Include="**\*.cs" />
    <EmbeddedResource Include="**\*.resx" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Build.Runtime">
      <Version>15.1.0-preview-000370-00</Version>
    </PackageReference>
    <PackageReference Include="Microsoft.NETCore.App">
      <Version>1.0.1</Version>
    </PackageReference>
    <PackageReference Include="Microsoft.NET.Sdk">
      <Version>1.0.0-alpha-20161104-2</Version>
      <PrivateAssets>All</PrivateAssets>
    </PackageReference>
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
jeffkl commented 7 years ago

@emgarten is this a bug in NuGet? The project.assets.json has the correct stuff:

"Microsoft.Build.Runtime/15.1.0-preview-000370-00": {
  "type": "package",
  "dependencies": {
    "Microsoft.Build": "[15.1.0-preview-000370-00]",
    "Microsoft.Build.Framework": "[15.1.0-preview-000370-00]",
    "Microsoft.Build.Tasks.Core": "[15.1.0-preview-000370-00]",
    "Microsoft.Build.Utilities.Core": "[15.1.0-preview-000370-00]"
  },
  "contentFiles": {
    "contentFiles/any/netcoreapp1.0/15.0/Microsoft.Common.props": {
      "buildAction": "None",
      "codeLanguage": "any",
      "copyToOutput": true,
      "outputPath": "15.0/Microsoft.Common.props"
    },
    "contentFiles/any/netcoreapp1.0/MSBuild.dll": {
      "buildAction": "None",
      "codeLanguage": "any",
      "copyToOutput": true,
      "outputPath": "MSBuild.dll"
    },
    "contentFiles/any/netcoreapp1.0/MSBuild.runtimeconfig.json": {
      "buildAction": "None",
      "codeLanguage": "any",
      "copyToOutput": true,
      "outputPath": "MSBuild.runtimeconfig.json"
    },
    "contentFiles/any/netcoreapp1.0/Microsoft.CSharp.CrossTargeting.targets": {
      "buildAction": "None",
      "codeLanguage": "any",
      "copyToOutput": true,
      "outputPath": "Microsoft.CSharp.CrossTargeting.targets"
    },
    // (etc)
  }
},

But the files are not in the output folder after doing dotnet build. Or is this logic part of the SDK?

C:\Users\jeffkl\Downloads\msbuildruntimerepro>dotnet new
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp1.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="**\*.cs" />
    <EmbeddedResource Include="**\*.resx" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NETCore.App">
      <Version>1.0.1</Version>
    </PackageReference>
    <PackageReference Include="Microsoft.NET.Sdk">
      <Version>1.0.0-alpha-20161104-2</Version>
      <PrivateAssets>All</PrivateAssets>
    </PackageReference>
+   <PackageReference Include="Microsoft.Build.Runtime">
+     <Version>15.1.0-preview-000370-00</Version>
+   </PackageReference>
  </ItemGroup>

  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
C:\Users\jeffkl\Downloads\msbuildruntimerepro>dotnet restore
  Restoring packages for C:\Users\jeffkl\Downloads\msbuildruntimerepro\msbuildruntimerepro.csproj...
  Writing lock file to disk. Path: C:\Users\jeffkl\Downloads\msbuildruntimerepro\obj\project.assets.json
  Generating MSBuild file C:\Users\jeffkl\Downloads\msbuildruntimerepro\obj\msbuildruntimerepro.csproj.nuget.g.targets.
  Generating MSBuild file C:\Users\jeffkl\Downloads\msbuildruntimerepro\obj\msbuildruntimerepro.csproj.nuget.g.props.
  Restore completed in 1124.0981ms for C:\Users\jeffkl\Downloads\msbuildruntimerepro\msbuildruntimerepro.csproj.

  NuGet Config files used:
      C:\Users\jeffkl\AppData\Roaming\NuGet\NuGet.Config

  Feeds used:
      https://api.nuget.org/v3/index.json

C:\Users\jeffkl\Downloads\msbuildruntimerepro>dotnet build /v:m
Microsoft (R) Build Engine version 15.1.0.0
Copyright (C) Microsoft Corporation. All rights reserved.

  msbuildruntimerepro -> C:\Users\jeffkl\Downloads\msbuildruntimerepro\bin\Debug\netcoreapp1.0\msbuildruntimerepro.dll

C:\Users\jeffkl\Downloads\msbuildruntimerepro>tree /f bin\Debug\netcoreapp1.0
Folder PATH listing
Volume serial number is 4E82-251B
C:\USERS\JEFFKL\DOWNLOADS\MSBUILDRUNTIMEREPRO\BIN\DEBUG\NETCOREAPP1.0
    msbuildruntimerepro.deps.json
    msbuildruntimerepro.dll
    msbuildruntimerepro.pdb
    msbuildruntimerepro.runtimeconfig.dev.json
    msbuildruntimerepro.runtimeconfig.json

No subfolders exist
jeffkl commented 7 years ago

Okay this is currently blocked by https://github.com/NuGet/Home/issues/3683 because our contentFiles are not being copied to the output directory. The workaround is to set MSBUILD_EXE_PATH for now...

rainersigwald commented 7 years ago

@jeffkl What's the current state of this for a tool? My understanding is that it should work fine since dotnet will set MSBUILD_EXE_PATH to its MSBuild root and allow a project to have standard dependencies on our DLLs and evaluate a project using the CLI's toolset--right? I think @natemcmaster thinks otherwise . . .

natemcmaster commented 7 years ago

I haven't tested lately. We decided that ASP.NET Core CLI tools should not use MSBuild APIs for project evaluation. AFAIK the only tool using MSBuild object model programmatically is dotnet-prop (https://github.com/simonech/dotnet-prop).

Instead, ASP.NET Core CLI tools accomplish indirect project evaluation by abusing the imports from MSBuildProjectExtensionsPath . This allows us to inject targets into a project. See https://github.com/aspnet/DotNetTools/pull/206 for a brief description of how dotnet-watch implements that.

jeffkl commented 7 years ago

I'm not sure, I should probably set up a repro so I can test solutions.

We didn't really want people taking a dependency on MSBUILD_EXE_PATH but it should fix the problem for now. Longer term we talked about NuGet being able to resolve assets other than reference assemblies. Another option is to put MSBuild.exe next to Microsoft.Build.dll in the appropriate package. I just need to take the time to set this up so I can test it all...

natemcmaster commented 6 years ago

Ping on a really old thread. At this point, all of the tools I have created workaround this limitation by invoking a new MSBuild process instead of calling on MSBuild API directly. It's a less-than-ideal programming experience though.

I recently saw this: https://github.com/daveaglick/Buildalyzer. Is this kind of API something MSBuild would every provide as a 1st class thing? If not, I suugest we just close this as "wontfix" and invite tool authors to use something like https://github.com/daveaglick/Buildalyzer if they want to use MSBuild in-proc.