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.jsonalready 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.TargetFrameworkAttributeattributeDuplicate
System.Reflection.AssemblyCompanyAttributeattributeDuplicate
System.Reflection.AssemblyConfigurationAttributeattributeDuplicate
System.Reflection.AssemblyFileVersionAttributeattributeDuplicate
System.Reflection.AssemblyInformationalVersionAttributeattributeDuplicate
System.Reflection.AssemblyProductAttributeattributeDuplicate
System.Reflection.AssemblyTitleAttributeattributeDuplicate
System.Reflection.AssemblyVersionAttributeattribute
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.jsondoesn't have a target fornet8.0/linux-x64. Ensure that restore has run and that you have includednet8.0in theTargetFrameworksfor your project. You may also need to includelinux-x64in 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.
