Reputation: 63124
I am trying to run MSBuild programmatically from a C# DLL (which will ultimately be loaded from PowerShell), and as a first step from a command-line application. I have used Microsoft.Build.Locator as recommended (or so I reckon) by installing its NuGet package to my project, and adding the following references to my test project:
<Reference Include="Microsoft.Build, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.Build.Framework, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.Build.Locator, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9dff12846e04bfbd, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Build.Locator.1.2.6\lib\net46\Microsoft.Build.Locator.dll</HintPath>
</Reference>
The project targets .NET Framework 4.8, and the source code is as follows:
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Locator;
using System.Collections.Generic;
namespace nrm_testing
{
class Program
{
static void Main(string[] args)
{
MSBuildLocator.RegisterDefaults();
DoStuff();
}
static void DoStuff()
{
using (var projectCollection = new ProjectCollection())
{
var buildParameters = new BuildParameters
{
MaxNodeCount = 1 // https://stackoverflow.com/q/62658963/3233393
};
var buildRequestData = new BuildRequestData(
@"path\to\a\project.vcxproj",
new Dictionary<string, string>(),
null,
new string[0],
null
);
var result = BuildManager.DefaultBuildManager.Build(buildParameters, buildRequestData);
}
}
}
}
Upon entering the using
block, I receive the following exception:
System.IO.FileNotFoundException: 'Could not load file or assembly 'System.Runtime.CompilerServices.Unsafe, Version=4.0.4.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The system cannot find the file specified.'
The Modules window shows that MSBL did successfully locate my VS2019 installation:
Microsoft.Build.dll 16.07.0.37604 C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\Microsoft.Build.dll
Microsoft.Build.Framework.dll 16.07.0.37604 C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\Microsoft.Build.Framework.dll
Microsoft.Build.Locator.dll 1.02.6.49918 C:\dev\nrm3-tests\nrm\nrm-testing\.out\AnyCPU-Debug\Microsoft.Build.Locator.dll
System.Runtime.CompilerServices.Unsafe.dll
is indeed present besides the located MSBuild assemblies, in version 4.0.6.0 (according to DotPeek).
What could be causing this error, and how could I fix it?
My attempts so far:
I have found this question, but the linked GitHub issue is still open and I'm unsure whether it's the same problem.
I have managed to get a binding redirection working, but I don't think I can use them from within a DLL, so that's a dead end.
Adding the System.Runtime.CompilerServices.Unsafe
NuGet package to the project (and verifying that it is indeed copied alongside the project's executable) does nothing (thanks magicandre1981 for the suggestion).
Switching from packages.config to PackageReference (as suggested by Perry Qian), with no change in behaviour.
Upvotes: 0
Views: 1391
Reputation: 63124
After a lot of fiddling with different ideas, I ended up writing this workaround based on manual assembly resolution.
RegisterMSBuildAssemblyPath
detects when Microsoft.Build.dll
gets loaded, and memorizes its directory. Upon subsequent assembly load failures, RedirectMSBuildAssemblies
checks if the missing assembly exists inside that path, and loads it if it does.
class Program
{
private static string MSBuildAssemblyDir;
static void Main(string[] args)
{
MSBuildLocator.RegisterDefaults();
Thread.GetDomain().AssemblyLoad += RegisterMSBuildAssemblyPath;
Thread.GetDomain().AssemblyResolve += RedirectMSBuildAssemblies;
DoStuff();
}
private static void RegisterMSBuildAssemblyPath(object sender, AssemblyLoadEventArgs args)
{
var assemblyPath = args.LoadedAssembly.Location;
if (Path.GetFileName(assemblyPath) == "Microsoft.Build.dll")
MSBuildAssemblyDir = Path.GetDirectoryName(assemblyPath);
}
private static Assembly RedirectMSBuildAssemblies(object sender, ResolveEventArgs args)
{
if (MSBuildAssemblyDir == null)
return null;
try
{
var assemblyFilename = $"{args.Name.Split(',')[0]}.dll";
var potentialAssemblyPath = Path.Combine(MSBuildAssemblyDir, assemblyFilename);
return Assembly.LoadFrom(potentialAssemblyPath);
}
catch (Exception)
{
return null;
}
}
static void DoStuff()
{
// Same as before
}
}
I'm pretty sure there are (many) corner cases that will make this fail, but it will do for now.
Upvotes: 1
Reputation: 23780
Actually, this is an real issue for a long time for packages.config nuget management format. And Microsoft's recommended solution for this problem is to add a bindingRedirect.
Usually, you can use this node in xxx.csproj
file to automatically generate bindingredirect.
However, for some specific dlls, this node may not work due to serveral reasons. And it is still an issue on the current packages.config nuget management format
as your said.
Suggestion
As a suggestion, you could use the new PackageReference nuget manage format instead since VS2017. This format is simple, convenient and efficient.
Also, when you use this format, first, you should make a backup of your project.
Just right-click on the packages.config
file-->click Migrate packages.config to PackageReference
.
Besides, I have also reported this issue on DC Forum and I hope the team will provide a better suggestion.
Upvotes: 0