Troubleshooting MSBuild

This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its dotnet CLI) and NuGet 7.9, as published in the MSBuild, .NET and NuGet documentation on learn.microsoft.com, which is the reference these pages are written and verified against. Material about .NET 11 / NuGet 8.0 is still in preview and is always flagged as such — never presented as baseline.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The C# Reference bibliography lists the reference material consulted while preparing these pages.

Most MSBuild problems are one of two kinds: a value is not what you expected (a property, an item list, an import), or a task failed. Both are solved faster with a fixed method than by editing the project and rebuilding. This page gives that method, then covers the failures that need specific knowledge, with MSBuild 18.9 (.NET SDK 10.0.402).

A diagnosis method

Work from the cheapest tool to the most detailed one, and stop as soon as you have the answer.

  1. Ask for the value. -getProperty, -getItem and -getTargetResult print evaluated values as JSON without building (Command-Line Reference).

  2. Find where it was set. -pp:<file> writes the whole project with every import inlined and a comment naming each imported file; search it for the property.

  3. Watch the build. A binary log (-bl) records every evaluation, property assignment, target, task and message; the MSBuild Structured Log Viewer searches it (Logging and Binary Logs).

  4. Read everything. -v:diag (or -v:d) in the console, when you cannot open a viewer — for example in a CI log.

A worked example

Ledgerline packs the library and the CLI, but not the test project or the sample. Why is IsPackable false for Ledgerline.Tests?

for p in src/Ledgerline src/Ledgerline.Cli tests/Ledgerline.Tests; do
  echo "$p $(dotnet msbuild $p -getProperty:IsPackable)"
done
dotnet msbuild src/Ledgerline.Cli -pp:cli.pp.xml
src/Ledgerline true
src/Ledgerline.Cli true
tests/Ledgerline.Tests false

Searching cli.pp.xml for <IsPackable and noting the file comment above each hit gives the whole story:

111   ~/ledgerline/Directory.Build.props                         <IsPackable>false</IsPackable>
1532  ~/ledgerline/src/Ledgerline.Cli/Ledgerline.Cli.csproj      <IsPackable>true</IsPackable>
17075 NuGet.Build.Tasks.Pack.targets  <IsPackable Condition="'$(IsPackable)'=='' AND ('$(IsTestProject)'=='true' OR '$(IsTestingPlatformApplication)'=='true')">false</IsPackable>
17076 NuGet.Build.Tasks.Pack.targets  <IsPackable Condition="'$(IsPackable)'==''">true</IsPackable>

Directory.Build.props turns packing off for every project, and only the projects that should be packed turn it back on. The NuGet defaults never apply in Ledgerline, because the property is never empty when they run. The same three steps answer "why is this file compiled?", "why does this target run?" and "which .targets file defines this?".

Task failures

Every task failure names the file and line of the task element. Start there:

  • MSB4018 "task failed unexpectedly" — the task threw an exception; the stack trace follows in the log. The bug is in the task (or its input), not in the project.

  • MSB4181 "returned false but did not log an error" — the task failed silently; fix the task to log why (Writing Custom Tasks).

  • MSB6006 "<tool> exited with code N" — a ToolTask ran an executable that failed without the task recognising its error format. Run the logged command line by hand, or raise verbosity to see the tool’s output.

  • MSB3073 — the same for the Exec task; Exec prints the tool’s output above the error.

To debug a custom task, set MSBUILDDEBUGONSTART. On Windows 1 launches the JIT debugger; on any platform 2 prints the process ID and waits for a debugger to attach:

MSBUILDDEBUGONSTART=2 dotnet msbuild cw.proj -t:Show
Waiting for debugger to attach (~/.dotnet/dotnet PID 29340).  Press enter to continue...

Build with -m:1 -nr:false while debugging, so that the task runs in the process you attached to and a stale copy of the task assembly is not reused from an earlier node.

Assembly-reference conflicts

ResolveAssemblyReference (RAR) turns Reference items and the assemblies from packages into the files the compiler references and the build copies. When two versions of one assembly meet, it picks one. The documentation describes warning MSB3277, "Found conflicts between different versions of '…' that could not be resolved", for conflicts it cannot settle. In the reproduction below — with both net10.0 and net48 — RAR settled the conflict the wrong way without any warning, and the program failed at run time.

The reproduction: Ledger.A.dll was compiled against Ledger.Core 1.0, Ledger.B.dll against Ledger.Core 2.0, and the application references both plus Ledger.Core 1.0 directly:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <OutputType>Exe</OutputType>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="Ledger.A" HintPath="../lib/A/Ledger.A.dll" />
    <Reference Include="Ledger.B" HintPath="../lib/B/Ledger.B.dll" />
    <Reference Include="Ledger.Core" HintPath="../lib/core1/Ledger.Core.dll" />
  </ItemGroup>
</Project>

dotnet build reports no warning, even with -p:AutoUnifyAssemblyReferences=false or a net48 target. Running the program fails:

Unhandled exception. System.IO.FileNotFoundException: Could not load file or assembly 'Ledger.Core, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null'. The system cannot find the file specified.

File name: 'Ledger.Core, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null'
   at Ledger.B.Use..ctor() in ~/rar/B/U.cs:line 1

The decision is in the log at detailed verbosity (dotnet build -v:d), under the RAR task:

There was a conflict between "Ledger.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" and "Ledger.Core, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null".
    "Ledger.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" was chosen because it was primary and "Ledger.Core, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null" was not.
    References which depend on "Ledger.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" [~/rar/lib/core1/Ledger.Core.dll].
        ~/rar/lib/core1/Ledger.Core.dll
          Project file item includes which caused reference "~/rar/lib/core1/Ledger.Core.dll".
            Ledger.Core
            Ledger.A

A primary reference (one named in the project) always wins over a dependency. The fix is to reference the version the dependencies need (2.0), or — the usual case — to use PackageReference, where NuGet chooses one version for the whole graph (Dependency Resolution). Search a binary log for "There was a conflict"; set MSBUILDLOGVERBOSERARSEARCHRESULTS=1 to log every location RAR probed.

Missing targeting packs

SDK-style projects targeting .NET Framework download the reference assemblies as the Microsoft.NETFramework.ReferenceAssemblies package, so net48 builds on Linux and macOS. Turning that off (or a classic project on a machine without the Developer Pack) gives MSB3644:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net48</TargetFramework>
    <AutomaticallyUseReferenceAssemblyPackages>false</AutomaticallyUseReferenceAssemblyPackages>
  </PropertyGroup>
</Project>
~/.dotnet/sdk/10.0.402/Microsoft.Common.CurrentVersion.targets(1259,5): error MSB3644: The reference assemblies for .NETFramework,Version=v4.8 were not found. To resolve this, install the Developer Pack (SDK/Targeting Pack) for this framework version or retarget your application. You can download .NET Framework Developer Packs at https://aka.ms/msbuild/developerpacks [~/fx/Fx.csproj]

Source: MSB3644.

File locks

Copy retries when the destination cannot be written. With MSBuild 18.9 a Copy with no Retries attribute retried 10 times, 1000 ms apart (the Copy task page still says the task default is zero); the Retries and RetryDelayMilliseconds parameters change that. Each retry is warning MSB3026; giving up is error MSB3027, followed by MSB3021 with the operating-system reason. On Windows the usual cause is a running process (the application under test, an antivirus scanner, or a build node from an earlier build) holding the output file:

warning MSB3026: Could not copy "journals/jan.ledger.csv" to "/proc/ledgerline/jan.ledger.csv". Beginning retry 1 in 100ms. Could not find file '/proc/ledgerline'.
error MSB3027: Could not copy "journals/jan.ledger.csv" to "/proc/ledgerline/jan.ledger.csv". Exceeded retry count of 2. Failed.
error MSB3021: Unable to copy file "journals/jan.ledger.csv" to "/proc/ledgerline/jan.ledger.csv". Could not find file '/proc/ledgerline'.

(This output comes from the Copy example in Tasks and Built-in Tasks, which forces the failure with an unwritable path and Retries="2".) Stop the process that holds the file, run dotnet build-server shutdown to end reused nodes, and look for two projects writing the same output (BuildCheck BC0101, BuildCheck, Change Waves and Best Practices).

Source: MSB3026, MSB3027.

Error table

Every message below was produced with MSBuild 18.9 (paths shortened); the "Shown in" column links the page with the reproduction.

Code Message Usual cause and fix

MSB1009

Project file does not exist. Switch: nothere.csproj

A typo in the project argument, or a relative path from the wrong folder.

MSB1011

Specify which project or solution file to use because this folder contains more than one project or solution file.

Name the file; see Solutions and .slnx.

MSB3021

Unable to copy file "…" to "…". Could not find file '/proc/ledgerline'.

The final copy failure, with the OS reason.

MSB3026

Could not copy "…" to "…". Beginning retry 1 in 100ms. …

A locked or unwritable destination; see File locks.

MSB3027

Could not copy "…" to "…". Exceeded retry count of 2. Failed.

The retries ran out.

MSB3073

The command "grep -q 9999-Unknown journals/jan.ledger.csv" exited with code 1.

An Exec command failed; its output is above the error (Tasks and Built-in Tasks).

MSB3277

Found conflicts between different versions of 'assembly-name' that could not be resolved. (text from the MSB3277 page; not reproduced with SDK 10.0.402)

Two versions of one assembly; see Assembly-reference conflicts.

MSB3644

The reference assemblies for .NETFramework,Version=v4.8 were not found. …

A missing targeting pack; see Missing targeting packs.

MSB4018

The "ValidateLedger" task failed unexpectedly. followed by the exception

A bug in the task (Writing Custom Tasks).

MSB4019

The imported project "~/demo/build/ledger.targets" was not found. …

A wrong Import path (Imports and Project SDKs).

MSB4057

The target "Deploy" does not exist in the project.

A misspelled -t: target, or a target defined in a file that was not imported (Targets and Build Order).

MSB4062

The "Ledgerline.Build.ValidateLedger" task could not be loaded from the assembly …

A wrong UsingTask path, or a missing dependency of the task assembly.

MSB4181

The "SilentFailure" task returned false but did not log an error.

A task that fails without logging.

MSB4236

The SDK 'Microsoft.Build.NoTargets' specified could not be found.

An MSBuild SDK without a version in the project or global.json (Imports and Project SDKs).

Common mistakes

  • Editing and rebuilding instead of asking — -getProperty answers in a second.

  • Reading only the last error — the first error, or a warning above it, usually names the cause.

  • Debugging a custom task with node reuse on — the attached process is not the one running the task.

  • Trusting a clean build for references — RAR conflicts can build silently; run the program and its tests.

  • Retrying a locked-file build without stopping the holder — the next build hits the same lock.