Skip to content

Commit 7947ef5

Browse files
authored
Make the MSBuild runtime assembly check more correct (#360)
* make the MSbuild runtime assembly check more correct * create separate diagnostics for each triggering package, and provide code + link to enable searching * Add count condition to ensure that empty diagnostics messages are not raised
1 parent c6c6fed commit 7947ef5

File tree

5 files changed

+150
-28
lines changed

5 files changed

+150
-28
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ That additional build logic is distributed with Visual Studio, with Visual Studi
88

99
Loading MSBuild from Visual Studio also ensures that your application gets the same view of projects as `MSBuild.exe`, `dotnet build`, or Visual Studio, including bug fixes, feature additions, and performance improvements that may come from a newer MSBuild release.
1010

11-
## How Locator searches for .NET SDK?
11+
## How does Locator search for the .NET SDK?
1212

1313
MSBuild.Locator searches for the locally installed SDK based on the following priority:
1414

docs/diagnostics/MSBL001.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# MSBL001 - MSBuild runtime package is referenced without Runtime Assets excluded
2+
3+
## Error Message
4+
5+
> A PackageReference to the package '{PackageId}' at version '{Version}' is present in this project without ExcludeAssets="runtime" set. This can cause errors at run-time due to MSBuild assembly-loading.
6+
7+
## Cause
8+
9+
This error occurs when your project references MSBuild NuGet packages (such as `Microsoft.Build`, `Microsoft.Build.Framework`, `Microsoft.Build.Utilities.Core`, etc.) without excluding their runtime assets. When you use Microsoft.Build.Locator, you want MSBuildLocator to load MSBuild assemblies from an installed Visual Studio or .NET SDK instance, not from the NuGet packages in your output directory.
10+
11+
## Why This Is a Problem
12+
13+
When MSBuild runtime assemblies are copied to your application's output directory, your application may load these assemblies instead of the MSBuild installation that MSBuildLocator registered. This can lead to several runtime issues:
14+
15+
1. **Assembly version conflicts**: Your application loads MSBuild assemblies from your output directory while MSBuildLocator tries to load from a different MSBuild installation
16+
2. **Missing SDKs and build logic**: The MSBuild assemblies in your output directory don't include the SDKs, targets, and build logic needed to build real projects
17+
3. **Inconsistent behavior**: Your application may behave differently than `MSBuild.exe`, `dotnet build`, or Visual Studio when evaluating projects
18+
19+
## Example Runtime Error
20+
21+
Without `ExcludeAssets="runtime"`, you may encounter errors like:
22+
23+
```
24+
System.IO.FileNotFoundException: Could not load file or assembly 'Microsoft.Build, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies.
25+
```
26+
27+
Or:
28+
29+
```
30+
System.InvalidOperationException: SDK 'Microsoft.NET.Sdk' could not be resolved. The SDK resolver "Microsoft.DotNet.MSBuildSdkResolver" returned null.
31+
```
32+
33+
This happens because your application loads MSBuild assemblies from your bin folder (e.g., version 15.5.180) while MSBuildLocator has registered a different MSBuild installation (e.g., version 17.0) to use at runtime. The .NET runtime gets confused about which assemblies to use, leading to version conflicts and missing dependencies.
34+
35+
## Solution
36+
37+
Add `ExcludeAssets="runtime"` to all MSBuild PackageReferences in your project file:
38+
39+
```xml
40+
<ItemGroup>
41+
<PackageReference Include="Microsoft.Build" ExcludeAssets="runtime" />
42+
<PackageReference Include="Microsoft.Build.Framework" ExcludeAssets="runtime" />
43+
<PackageReference Include="Microsoft.Build.Utilities.Core" ExcludeAssets="runtime" />
44+
...
45+
</ItemGroup>
46+
```
47+
48+
This tells NuGet to use these packages only for compilation, not at runtime. At runtime, MSBuildLocator will load MSBuild assemblies from the registered Visual Studio or .NET SDK installation.
49+
50+
## Alternative: Disable the Check (Not Recommended)
51+
52+
If you need to distribute MSBuild assemblies with your application (not recommended), you can disable this check by setting the following property in your project file:
53+
54+
```xml
55+
<PropertyGroup>
56+
<DisableMSBuildAssemblyCopyCheck>true</DisableMSBuildAssemblyCopyCheck>
57+
</PropertyGroup>
58+
```
59+
60+
**Warning**: Disabling this check means you must distribute all of MSBuild and its associated toolset with your application, which is generally not recommended. The MSBuild team does not support this scenario, and you may encounter issues with SDK resolution and build logic.
61+
62+
## Related Documentation
63+
64+
- [Use Microsoft.Build.Locator](https://learn.microsoft.com/visualstudio/msbuild/updating-an-existing-application#use-microsoftbuildlocator)
65+
- [MSBuildLocator on GitHub](https://github.com/microsoft/MSBuildLocator)
66+

src/MSBuildLocator/Microsoft.Build.Locator.csproj

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<Title>MSBuild Locator</Title>
1313
<Description>Package that assists in locating and using a copy of MSBuild installed as part of Visual Studio 2017 or higher or .NET Core SDK 2.1 or higher.</Description>
1414
<PackageTags>msbuildlocator;locator;buildlocator</PackageTags>
15+
<PackageReadmeFile>README.md</PackageReadmeFile>
1516
<EnablePackageValidation>true</EnablePackageValidation>
1617
<PackageValidationBaselineVersion>1.6.1</PackageValidationBaselineVersion>
1718
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@@ -25,14 +26,11 @@
2526
</ItemGroup>
2627
<ItemGroup>
2728
<PackageReference Include="MicroBuild.Core" Version="0.3.0" PrivateAssets="all" />
28-
<Content Include="build\Microsoft.Build.Locator.props">
29-
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
30-
<PackagePath>build\</PackagePath>
31-
</Content>
32-
<Content Include="build\Microsoft.Build.Locator.targets">
33-
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
34-
<PackagePath>build\</PackagePath>
35-
</Content>
29+
<None Include="README.md" Pack="true" PackagePath="\" />
30+
<None Include="build\Microsoft.Build.Locator.props" Pack="true" PackagePath="build\" />
31+
<None Include="build\Microsoft.Build.Locator.targets" Pack="true" PackagePath="build\" />
32+
<None Include="build\Microsoft.Build.Locator.props" Pack="true" PackagePath="buildTransitive/" />
33+
<None Include="build\Microsoft.Build.Locator.targets" Pack="true" PackagePath="buildTransitive/" />
3634
</ItemGroup>
3735
<ItemGroup>
3836
<FilesToSign Include="$(OutDir)\Microsoft.Build.Locator.dll">

src/MSBuildLocator/README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Microsoft.Build.Locator
2+
3+
Microsoft.Build.Locator helps you locate and register MSBuild assemblies provided with Visual Studio or the .NET SDK. This is essential when you need to use MSBuild APIs in your application to evaluate or build projects.
4+
5+
## Why do I need this?
6+
7+
When using MSBuild's .NET API to load and build projects, you need access to the SDKs and build logic distributed with Visual Studio or the .NET SDK, not just the MSBuild APIs. MSBuildLocator helps you find these installations and set up your application to use them, ensuring your code gets the same view of projects as `MSBuild.exe`, `dotnet build`, or Visual Studio.
8+
9+
## Quick Start
10+
11+
Before using any MSBuild APIs, register MSBuild assemblies:
12+
13+
```csharp
14+
using Microsoft.Build.Locator;
15+
using Microsoft.Build.Evaluation;
16+
17+
// Register defaults before using any MSBuild types
18+
MSBuildLocator.RegisterDefaults();
19+
20+
// Now you can safely use MSBuild APIs.
21+
// NOTE: due the the way that the CLR loads assemblies, you MUST
22+
// register MSBuild through Locator before any types from
23+
// the MSBuild assemblies are used in your application.
24+
// The safest way to ensure this is to put any MSBuild API
25+
// access into a separate method.
26+
LoadProject();
27+
28+
void LoadProject()
29+
{
30+
var project = new Project("MyProject.csproj");
31+
...
32+
}
33+
```
34+
35+
For more control over which MSBuild instance to use:
36+
37+
```csharp
38+
using Microsoft.Build.Locator;
39+
40+
// Query available MSBuild instances
41+
var instances = MSBuildLocator.QueryVisualStudioInstances().ToArray();
42+
43+
// Register a specific instance
44+
var instance = instances.OrderByDescending(i => i.Version).First();
45+
MSBuildLocator.RegisterInstance(instance);
46+
```
47+
48+
## Documentation
49+
50+
For complete documentation, see [Use Microsoft.Build.Locator](https://learn.microsoft.com/visualstudio/msbuild/updating-an-existing-application#use-microsoftbuildlocator) on Microsoft Learn.
51+
52+
## Samples
53+
54+
See the [BuilderApp](https://github.com/microsoft/MSBuildLocator/blob/a349ee7ffd889cd7634d3fd8b413bf9f29244b50/samples/BuilderApp) sample for a full
55+
exploration of the MSBuildLocator library and capabilities.
56+
57+
Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
2-
<Target Name="EnsureMSBuildAssembliesNotCopied" AfterTargets="Build" Condition="'$(DisableMSBuildAssemblyCopyCheck)' != 'true'">
2+
<Target Name="EnsureMSBuildAssembliesNotCopied" AfterTargets="ResolvePackageAssets" Condition="'$(DisableMSBuildAssemblyCopyCheck)' != 'true'">
33
<ItemGroup>
4-
<MSBuildPackagesWithoutPrivateAssets
5-
Include="@(PackageReference)"
6-
Condition="!$([MSBuild]::ValueOrDefault('%(PackageReference.ExcludeAssets)', '').ToLower().Contains('runtime')) and
7-
(
8-
'%(PackageReference.Identity)' == 'Microsoft.Build' or
9-
'%(PackageReference.Identity)' == 'Microsoft.Build.Framework' or
10-
'%(PackageReference.Identity)' == 'Microsoft.Build.Utilities.Core' or
11-
'%(PackageReference.Identity)' == 'Microsoft.Build.Tasks.Core' or
12-
'%(PackageReference.Identity)' == 'Microsoft.Build.Engine' or
13-
'%(PackageReference.Identity)' == 'Microsoft.Build.Conversion.Core' or
14-
'%(PackageReference.Identity)' == 'Microsoft.Build.Runtime' or
15-
'%(PackageReference.Identity)' == 'Microsoft.Build.Localization' or
16-
'%(PackageReference.Identity)' == 'Microsoft.Build.Engine' or
17-
'%(PackageReference.Identity)' == 'Microsoft.NET.StringTools' or
18-
'%(PackageReference.Identity)' == 'NuGet.Frameworks'
19-
)"/>
4+
<_MSBuildAssembliesCopyLocalItems Include="@(RuntimeCopyLocalItems->WithMetadataValue('NuGetPackageId', 'Microsoft.Build'))" />
5+
<_MSBuildAssembliesCopyLocalItems Include="@(RuntimeCopyLocalItems->WithMetadataValue('NuGetPackageId', 'Microsoft.Build.Framework'))" />
6+
<_MSBuildAssembliesCopyLocalItems Include="@(RuntimeCopyLocalItems->WithMetadataValue('NuGetPackageId', 'Microsoft.Build.Utilities.Core'))" />
7+
<_MSBuildAssembliesCopyLocalItems Include="@(RuntimeCopyLocalItems->WithMetadataValue('NuGetPackageId', 'Microsoft.Build.Tasks.Core'))" />
8+
<_MSBuildAssembliesCopyLocalItems Include="@(RuntimeCopyLocalItems->WithMetadataValue('NuGetPackageId', 'Microsoft.Build.Engine'))" />
9+
<_MSBuildAssembliesCopyLocalItems Include="@(RuntimeCopyLocalItems->WithMetadataValue('NuGetPackageId', 'Microsoft.Build.Conversion.Core'))" />
10+
<_MSBuildAssembliesCopyLocalItems Include="@(RuntimeCopyLocalItems->WithMetadataValue('NuGetPackageId', 'Microsoft.Build.Runtime'))" />
11+
<_MSBuildAssembliesCopyLocalItems Include="@(RuntimeCopyLocalItems->WithMetadataValue('NuGetPackageId', 'Microsoft.Build.Localization'))" />
12+
<_MSBuildAssembliesCopyLocalItems Include="@(RuntimeCopyLocalItems->WithMetadataValue('NuGetPackageId', 'Microsoft.Build.Engine'))" />
13+
<_MSBuildAssembliesCopyLocalItems Include="@(RuntimeCopyLocalItems->WithMetadataValue('NuGetPackageId', 'Microsoft.NET.StringTools'))" />
14+
<_MSBuildAssembliesCopyLocalItems Include="@(RuntimeCopyLocalItems->WithMetadataValue('NuGetPackageId', 'NuGet.Frameworks'))" />
15+
<MSBuildAssembliesCopyLocalItems Include="@(_MSBuildAssembliesCopyLocalItems->WithMetadataValue('CopyLocal', 'true')->WithMetadataValue('AssetType', 'runtime'))" />
16+
<_DistinctMSBuildPackagesReferencedPoorly Include="@(MSBuildAssembliesCopyLocalItems->Metadata('NuGetPackageId')->Distinct())" />
2017
</ItemGroup>
18+
2119
<Error
22-
Condition="'@(MSBuildPackagesWithoutPrivateAssets)' != ''"
23-
Text="A PackageReference to Microsoft.Build.* without ExcludeAssets=&quot;runtime&quot; exists in your project. This will cause MSBuild assemblies to be copied to your output directory, causing your application to load them at runtime. To use the copy of MSBuild registered by MSBuildLocator, set ExcludeAssets=&quot;runtime&quot; on the MSBuild PackageReferences. To disable this check, set the property DisableMSBuildAssemblyCopyCheck=true in your project file (not recommended as you must distributed all of MSBuild + associated toolset). Package(s) referenced: @(MSBuildPackagesWithoutPrivateAssets)" />
20+
Condition="@(_DistinctMSBuildPackagesReferencedPoorly->Count()) > 0"
21+
Code="MSBL001"
22+
Text="A PackageReference to the package '%(_DistinctMSBuildPackagesReferencedPoorly.NuGetPackageId)' at version '%(_DistinctMSBuildPackagesReferencedPoorly.NuGetPackageVersion)' is present in this project without ExcludeAssets=&quot;runtime&quot; set. This can cause errors at run-time due to MSBuild assembly-loading."
23+
HelpLink="https://aka.ms/msbuild/locator/diagnostics/MSBL001"
24+
/>
2425
</Target>
2526
</Project>

0 commit comments

Comments
 (0)