MSBuild: unnecessary rebuilds because of generated AssemblyAttributes.cs
A surprisingly common cause for unnecessary rebuilds of C# MSBuild projects is an unfortunate design in a part of MSBuild tooling that deals with generating AssemblyAttributes.cs. See for example this bug on Connect: https://connect.microsoft.com/VisualStudio/feedback/details/1269019/visualstudio-caches-wrong-path-for-netframework-version-v4-5-assemblyattributes-cs-file
But let us step back.
Let’s create a new C# Console Application in Visual Studio and build it with diagnostic verbosity. The most commonly useful tool to investigate build issues is the MSBuild diagnostic log:
msbuild.exe ConsoleApplication1.csproj /fl1 /noconlog /v:diag
(fl1 = write log to file msbuild1.log, noconlog = don’t write to console, v:diag = diagnostic verbosity)
When you open msbuild1.log and search for “csc”, you will see that the compiler invocation indeed includes a strange file:
csc.exe /noconfig /nowarn:1701,1702 /nostdlib+ /platform:anycpu32bitpreferred /errorreport:prompt /warn:4 /define:DEBUG;TRACE /highentropyva+ /reference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Microsoft.CSharp.dll" ... /debug+ /debug:full /filealign:512 /optimize- /out:obj\Debug\CS9.exe /subsystemversion:6.00 /target:exe /utf8output Program.cs Properties\AssemblyInfo.cs "C:\Users\kirill\AppData\Local\Temp\.NETFramework,Version=v4.5.AssemblyAttributes.cs"
Turns out, every time you build a C# project, MSBuild makes sure there’s a generated file in the TEMP directory called AssemblyAttributes.cs with content similar to this:
// <autogenerated />
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.5", FrameworkDisplayName = ".NET Framework 4.5")]
But how does it get there?
Let’s open the excellent free MSBuild Explorer tool (http://msbuildexplorer.com) and open our .csproj file:
In the list of properties we find the TargetFrameworkMonikerAssemblyAttributesPath MSBuild property which is the same path that’s passed to the C# compiler. But who sets this MSBuild property and where?
Let’s now open http://source.roslyn.io which happens to include semantically hyperlinked MSBuild targets for C# and search for our property name: http://source.roslyn.io/#q=TargetFrameworkMonikerAssemblyAttributesPath.
We indeed find an MSBuild property with this name and can view its declarations and usages:
We see the one place in the common targets where this property is written to:
Oh hey, look:
<TargetFrameworkMonikerAssemblyAttributesPath Condition="'$(TargetFrameworkMonikerAssemblyAttributesPath)' == ''">$([System.IO.Path]::Combine(' $([System.IO.Path]::GetTempPath()) ','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)'))</TargetFrameworkMonikerAssemblyAttributesPath>
This is the unfortunate place in MSBuild default targets that generates the file to the TEMP directory. Why is this bad? Well, it turns out, the location of TEMP may change if, for example, you use Remote Desktop to log on to your machine – the TEMP folder is different than your console sessions: http://blogs.msdn.com/b/oldnewthing/archive/2011/01/25/10119675.aspx
And so MSBuild sees that the file timestamp is newer than it remembers and decides to rebuild the project.
Let’s do an experiment. If you haven’t yet, go and set this registry key that I blogged about earlier. This will enable VS to tell you every time it decides to rebuild a project why it thought the project wasn’t up-to-date. Now build your console app once in VS (or in command line). Build again. Notice how it immediately says Build succeeded? Now go and edit that temporary file. Rebuild again – VS will decide that the project is out-of-date and rebuild it:
This is the problem that I mentioned in the beginning of this post. This breaks incrementality if multiple projects touch this file (and they often do!).
Fortunately for us, MSBuild is flexible enough so we can work around it. The good design is to generate this file into the Intermediate directory (usually called obj), because this is where all transient and temporary files should go during a build process. We can either set this property in our project file:
<PropertyGroup> <TargetFrameworkMonikerAssemblyAttributesPath>$([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)'))</TargetFrameworkMonikerAssemblyAttributesPath> </PropertyGroup>
Or if your build uses a common .props file, set this property there. This will ensure that your build doesn’t depend on the TEMP directory and is more isolated, repeatable and incremental.