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.21k stars 1.35k forks source link

Microsoft.Build API exception: Could not load file or assembly Microsoft.Build.Framework, Version=15.1.0.0 #8184

Open mattaquinn opened 1 year ago

mattaquinn commented 1 year ago

I am attempting to use the Microsoft.Build API to build projects and solution for use in an application within my company that is converting thousands of csproj files to the SDK format and verifying the build before and after. I am running into an exception with WPF projects that are in the Framework format. If I convert the project to SDK, the build succeeds. I do not have this issue building within Visual Studio 2022 or using MSBuild in 2022 Developer Command prompt. When I dig into the the exception I find that the root cause is an FileNotFoundException loading 'Microsoft.Build.Framework, Version=15.1.0.0,...'

I am not sure if there is a problem with my installation or something with my code/setup. I have attached the results of EnumerateMSBuild.ps1 and included as much detail as I can think of below. Please let me know if you require any more information and thank you in advance for any guidance.

msbuild_versions.txt

The Error I get from the build is:

C:\windows\Microsoft.NET\Framework\v4.0.30319\Microsoft.WinFx.targets(268,9): error MC1000: Unknown build error, 'Object reference not set to an instance of an object.' 
    0 Warning(s)
    1 Error(s)

The diagnostic level log (abridged) has this information around the error:

 Using "MarkupCompilePass1" task from assembly "PresentationBuildTasks, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35".
  Task "MarkupCompilePass1"
...
    Input: Assembly Reference file: 'C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8\Facades\System.Xml.XPath.XDocument.dll'.
    C:\windows\Microsoft.NET\Framework\v4.0.30319\Microsoft.WinFx.targets(268,9): error MC1000: Unknown build error, 'Object reference not set to an instance of an object.' 
  Done executing task "MarkupCompilePass1" -- FAILED.
Done building target "MarkupCompilePass1" in project "G1.UI.WPF.csproj" -- FAILED.

When I attach a debugger and decompile symbols, I find the null ref is in the error handler on the "linePos" line, but the actual exception "e" is a file not found.

\\MS.Internal.MarkupCompiler
    internal void OnError(Exception e)
    {
      if (this.Error == null)
        return;
      int lineNum = e is XamlParseException xamlParseException ? xamlParseException.LineNumber : 0;
      int linePos = xamlParseException != null ? xamlParseException.LinePosition : 0;
      string originalFilePath = this.SourceFileInfo.OriginalFilePath;
      this.Error((object) this, new MarkupErrorEventArgs(e, lineNum, linePos, originalFilePath));
    }

Actual exception:

System.IO.FileNotFoundException
  HResult=0x80070002
  Message=Could not load file or assembly 'Microsoft.Build.Framework, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The system cannot find the file specified.
  Source=PresentationBuildTasks
  StackTrace:
   at MS.Internal.CompilerWrapper.OnSourceFileResolve(Object sender, SourceFileResolveEventArgs e)
   at MS.Internal.MarkupCompiler.OnSourceFileResolve(FileUnit file)

Inner Exception 1:
FileNotFoundException: Could not load file or assembly 'Microsoft.Build.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The system cannot find the file specified.

Call Stack:
PresentationBuildTasks.dll!MS.Internal.MarkupCompiler.OnSourceFileResolve(MS.Internal.FileUnit file)
PresentationBuildTasks.dll!MS.Internal.MarkupCompiler.Initialize(MS.Internal.FileUnit sourceFile)
PresentationBuildTasks.dll!MS.Internal.MarkupCompiler.Compile(MS.Internal.CompilationUnit cu)
PresentationBuildTasks.dll!MS.Internal.CompilerWrapper.DoCompilation(string assemblyName, string language, string rootNamespace, MS.Internal.FileUnit[] fileList, bool isSecondPass)
[AppDomain (SlnToSdk.exe, #1) -> AppDomain (markupCompilationAppDomain, #2)]
Modules (abridged): Name Version AppDomain Path
PresentationBuildTasks.dll PresentationBuildTasks.dll 4.8.4084.0 built by: NET48REL1 [2] markupCompilationAppDomain C:\windows\Microsoft.Net\assembly\GAC_MSIL\PresentationBuildTasks\v4.0_4.0.0.0__31bf3856ad364e35\PresentationBuildTasks.dll
Microsoft.Build.Utilities.v4.0.dll Microsoft.Build.Utilities.v4.0.dll 4.8.4084.0 built by: NET48REL1 [2] markupCompilationAppDomain C:\windows\Microsoft.Net\assembly\GAC_MSIL\Microsoft.Build.Utilities.v4.0\v4.0_4.0.0.0__b03f5f7f11d50a3a\Microsoft.Build.Utilities.v4.0.dll
Microsoft.Build.Locator.dll Microsoft.Build.Locator.dll 1.05.5.21636 [1] SlnToSdk.exe C:\src\etdev\NET6Migration\SlnToSdk\bin\Debug\net48\Microsoft.Build.Locator.dll
Microsoft.Build.dll Microsoft.Build.dll 17.02.1.25201 [1] SlnToSdk.exe C:\Program Files (x86)\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\Microsoft.Build.dll
Microsoft.Build.Framework.dll Microsoft.Build.Framework.dll 17.02.1.25201 [1] SlnToSdk.exe C:\Program Files (x86)\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\Microsoft.Build.Framework.dll
SlnToSdk.exe SlnToSdk.exe 1.00.0.0 [1] SlnToSdk.exe C:\src\etdev\NET6Migration\SlnToSdk\bin\Debug\net48\SlnToSdk.exe

My code:

<!-- Directory.Packages.props -->
<Project>
    <PropertyGroup>
        <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
    </PropertyGroup>
    <ItemGroup>
       <PackageVersion Include="Microsoft.Build" Version="17.2.2" />
       <PackageVersion Include="Microsoft.Build.Locator" Version="1.5.5" />
 </ItemGroup>
</Project>

<!-- Directory.Build.targets -->
<Project>
 <Target Name="NukeAll">
        <RemoveDir Directories="$(BaseOutputPath)" /> 
        <RemoveDir Directories="$(BaseIntermediateOutputPath)" /> 
    </Target>
</Project>

<!-- .csproj --> 
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFrameworks>net48</TargetFrameworks>
    <Nullable>enable</Nullable>
    <LangVersion>latest</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Build" ExcludeAssets="runtime" />
    <PackageReference Include="Microsoft.Build.Locator" />
  </ItemGroup>
</Project>
//Program
var latestInstance = MSBuildLocator.QueryVisualStudioInstances().MaxBy(x => x.Version);
MSBuildLocator.RegisterInstance(latestInstance);
Build(args[0]);

BuildResult Build(string solutionOrProjectFile)
{
    var pc = new ProjectCollection();
    var parameters = new BuildParameters(pc)
    {
        Loggers = new[]
        {
            new Microsoft.Build.Logging.ConsoleLogger(),
            new Microsoft.Build.Logging.FileLogger()
            {
                Verbosity = LoggerVerbosity.Diagnostic
            }
        } 
    };

    var request = new BuildRequestData(
        projectFullPath: solutionOrProjectFile,
        globalProperties: new Dictionary<string, string>
        {
            {"Configuration", "Release"},
        },
        toolsVersion: null,
        targetsToBuild: new string[]
        {
            "NukeAll",
            "Restore",
            "Build"
        },
        hostServices: null,
        flags: BuildRequestDataFlags.ProvideProjectStateAfterBuild);

    var buildResult = BuildManager.DefaultBuildManager.Build(parameters, request);
    return buildResult;
}
mattaquinn commented 1 year ago

Digging into this deeper I am noticing that Microsoft.Build.* assemblies in C:\Program Files (x86)\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\ have FileName = 17.2.1.25201, but are signed as Microsoft.Build, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a

I am not sure if this is expected or a contributing factor.

I'll also note that if I GAC these assemblies, then they are found. Should they be GAC'd? I see other lower versioned Microsoft.Build.* assemblies in the GAC.

rainersigwald commented 1 year ago

Very interesting. I repro, and on my test project I can fix by adding

DisableInProcNode = true,

to the BuildParameters. That forces the actual task execution into a copy of MSBuild.exe spawned by the API as a worker node, rather than running it in-proc, which appears to work around a problem inside the task (I'm guessing caused by doing something with an AppDomain inside the task).

Can you try that?

Digging into this deeper I am noticing that Microsoft.Build.* assemblies in C:\Program Files (x86)\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\ have FileName = 17.2.1.25201, but are signed as Microsoft.Build, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a

I am not sure if this is expected or a contributing factor.

It's expected. We've kept the AssemblyVersion constant at 15.1.0.0 to minimize the need to update binding redirects when MSBuild updates, but the file version reports the "true" version.

I'll also note that if I GAC these assemblies, then they are found. Should they be GAC'd? I see other lower versioned Microsoft.Build.* assemblies in the GAC.

They should not be GACed. If you GAC them you'll run into problems on VS upgrades/updates and potentially break side-by-side installations of Visual Studio (for instance having Preview and release installed simultaneously).

mattaquinn commented 1 year ago

@rainersigwald Thank you! This did the trick.

        var parameters = new BuildParameters(pc)
        {
            Loggers = ...
            DisableInProcNode = true
        };
mattaquinn commented 1 year ago

It appears the above work-around creates a different problem, whenever a solution has a build dependency the build fails with

error MSB4025: The project file could not be loaded. Could not find file 'Path\to\Some.csproj.metaproj'.

This seems to explain the problem best: https://github.com/dotnet/msbuild/issues/3517

If set MSBuildEmitSolution=1 it will work, but (as intended) will leave .metaproj files hanging around afterwards.

@rainersigwald is there any other option I can try?

meirumeiru commented 1 year ago

I have the same problem. And, to me it seems to be a general problem (or misunderstanding) about how MSBuild finds something (e.g. the assemblies that it loads). Whenever I run MSBuild from my solution, it doesn't work or load the wrong MSBuild version. I had to pack it into its own AppDomain and register assembly paths in order to solve this. I guess, Visual Studio does something similar, but I didn't find out what it does. ... now, this seems to be a solution, but it causes problems, whenever something happens, that runs in its own domain (again, I have no idea how Visual Studio handles this so that all the settings is makes are still set when something runs in its own domain)... but here, with XAML compilation, we do have such a problem. My temporary solution was to set "AlwaysCompileMarkupFilesInSeparateDomain" to "false". ... but I have still other problems like e.g. I cannot build solutions with project dependencies, because all the "using" paths are wrong then.

so... would be nice to hear about how to prepare the "world" in order to be able to call MSBuild so that it then runs as if from within Visual Studio