Parallel builds for .NET projects
I had the pleasure of troubleshooting an issue that occurred on our build server after we tried to improve performance by parallelizing the build. We need to build the same project in several configurations, so we started running the dotnet publish
command for each configuration in parallel, of course with a different output folder for each one:
dotnet publish MyProject/MyProject.csproj --configuration MyConfig1 --output MyConfig1
Once we did that, occasionally one of the configurations failed with the following error:
The file
.../obj/project.assets.json
already exists.
Although by default each configuration uses a separate subfolder inside obj
for intermediate files, the project.assets.json
is put directly inside obj
and is thus shared across all builds. Which, obviously, can cause the error above when the timing is just right.
The first instinct for solving the issues was to specify custom intermediate path using the BaseIntermediateOutputPath
MSBuild parameter:
dotnet publish MyProject/MyProject.csproj --configuration MyConfig --output MyConfig /p:BaseIntermediateOutputPath=MyConfig-obj/
While this makes sure that the project.assets.json
files from different builds end up in different folders and therefore can't interfere with each other, it introduces a new set of problems resulting in several errors because of duplicate assembly-level attributes:
Duplicate
global::System.Runtime.Versioning.TargetFrameworkAttribute
attributeDuplicate
System.Reflection.AssemblyCompanyAttribute
attributeDuplicate
System.Reflection.AssemblyConfigurationAttribute
attributeDuplicate
System.Reflection.AssemblyFileVersionAttribute
attributeDuplicate
System.Reflection.AssemblyInformationalVersionAttribute
attributeDuplicate
System.Reflection.AssemblyProductAttribute
attributeDuplicate
System.Reflection.AssemblyTitleAttribute
attributeDuplicate
System.Reflection.AssemblyVersionAttribute
attribute
They occur because by default as part of the build a C# source file with assembly-level attributes is generated in the intermediate path, named MyProject.AssemblyInfo.cs
. And while the custom intermediate path currently in use is excluded from the project, that's not true for custom intermediate paths of other builds running in parallel. This means that the file generated in MyConfig1-obj
will be included in the build which uses MyConfig2-obj
.
Of course, there are ways to work around that, but fortunately there is a simpler way to resolve the original issue. Namely, the project.assets.json
file is generated during the restore phase of the build which happens implicitly as part of the dotnet publish
command. This can be avoided with the --no-restore
option.
dotnet publish MyProject/MyProject.csproj --no-restore --configuration MyConfig --output MyConfig
Now the file won't be generated by each of the builds running in parallel, so there's no chance for conflict. Just make sure to explicitly restore the packages with the dotnet restore
command before doing any builds which will also generate the project.assets.json
file:
dotnet restore
Also make sure to run the dotnet restore
command with the same runtime identifier which you're going to use for your build. For example, if you're going to publish your project for linux-x64
and you're building on Windows, use the --runtime linux-x64
option both for the initial dotnet restore
command and for the dotnet publish
command after that:
dotnet restore --runtime linux-x64
dotnet publish MyProject/MyProject.csproj --no-restore --configuration MyConfig --output MyConfig --runtime linux-x64 --self-contained false /p:PublishReadyToRun=true
Otherwise, the dotnet publish
command will complain that the restore hasn't run:
Assets file
MyProject\obj\project.assets.json
doesn't have a target fornet8.0/linux-x64
. Ensure that restore has run and that you have includednet8.0
in theTargetFrameworks
for your project. You may also need to includelinux-x64
in your project'sRuntimeIdentifiers
.
I was surprised that there isn't much documentation about parallel builds for .NET. This made it more difficult to find a proper solution than it should have been. At least, in the end it can all be explained: the project.assets.json
file is not configuration specific and is therefore placed in the root of the intermediate path instead of the configuration specific subfolder. Doing the restore beforehand instead of as part of each build also makes sense, both when running the builds in parallel or sequentially.