Wednesday, 2 October 2013

MSBuild gotcha in .net Framework 4.5

So I was originally going to put out the first point release of MapGuide Maestro 5.0 earlier this week, but I hit a snag in the build system that completely baffled me.

Since Google failed to reveal anything of substance on this particular issue, I figured I'd do the right and responsible thing and tell you what this problem is and how I fixed it so it can be indexed by Google so anyone else having this problem can just find this post :)

The problem

To illustrate, this is the set of build artifacts for Maestro 5.0

And here's the set of build artifacts of Maestro trunk

How on earth did the release zip file balloon out by 120MB? Well if we look at the build output directory that we then make our installer/zip files from, we see how it got to this size.

The FDO and CS-Map dictionaries for our Local Connection mode addin have been copied to the root of the output directory in addition to the addin's output directory. Our Maestro VS solution file has 48 projects. Some of these projects have unmanaged dlls and content/data files that we need to also copy across into the correct subdirectories of our main output directory.

Our build system wipes out the Local Connection mode addin directory before zipping (the zip release is our multi-platform release, where Local Connection mode is windows-only so we delete this directory), but because the FDO and CS-Map dictionary directories have been duplicated somewhere they're not supposed to be due to this particular issue, they've been inadvertently included in the zip file as a result

So that's the problem, but why did this happen?

The cause

In a nutshell, something changed with the MSBuild that is installed by .net Framework 4.5.

If you built the Maestro solution in the Visual Studio IDE (2010 or 2012) or with the MSBuild that comes with .net Framework 4.0. We got the expected file/directory structure that our build system then makes the installer/zip files from. But when we build Maestro with MSBuild 4.5, we get this strange file/directory structure.

A full verbose file-logged build with MSBuild 4.0 and MSBuild 4.5 and comparing the log files revealed the culprit.

It was these settings I had in my solution file.

I don't remember why the highlighted projects were ticked, but the highlighted items were indirect dependencies of Maestro.exe that I ticked some time in the past on the belief that building Maestro will then "pull in" these indirect project dependencies and cause them to be built as well. I did this because when I want to debug Maestro from Visual Studio for the first time, I want to make sure the addin projects were being built as well but because Maestro.exe does not reference these projects directly (it loads these addins at runtime), I had to use the project dependency mechanism in the solution settings to make sure these projects were being built when I hit F5 in Visual Studio.

It turns out with MSBuild 4.5, that these settings produced an undesirable (or is it desirable?) side-effect of any files marked Copy to output directory = Always from these projects also being copied over to the output directory of Maestro.exe in addition to their project's respective output directory.

The solution (for me)

The solution for me was to simply un-tick these dependent projects. I've conditioned myself to do a full solution build in Visual Studio anyway before I start debugging so when I hit F5, the addins will have already been built and ready to be loaded by the Maestro.exe I'm debugging, so having these projects ticked was not necessary.

Now if this is not an option. There were other options I explored that also worked for me with varying levels of feasibility

  • Use post-build events to copy the relevant content/data files instead of using the Copy to output directory item property. Even if triggered as an indirect dependency project by MSBuild, the post-build events will ensure any content/data files will be copied to their intended location. If you have lots of projects with extra files that need to be copied this may be a painstaking process.
  • Replace the Microsoft.Common.tasks file under %SystemRoot%\Microsoft.NET\Framework\v4.0.30319 with the .net Framework 4.0 copy. Because .net Framework 4.5 is an in-place upgrade over 4.0, the 4.0 copy of Microsoft.Common.tasks is replaced. This is not recommended as a viable solution. I just noted this because this worked for me. I guess the proper way to do this would be to use whatever APIs provided by MSBuild to override the various v4.5 MSBuild tasks/targets with their v4.0 equivalents to get the old behaviour. I'm not an MSBuild expert, so I can't really further elaborate on this or confirm what I just said about using MSBuild APIs is actually possible.

As for whether this behaviour in MSBuild 4.5 a bug or a feature, the jury's still out on that one. At first glance I thought it was a bug (because it sure didn't behave like this in MSBuild 3.5 and 4.0), but on second thought it does seem like a feature. If project A has manually marked dependencies B and C in the solution then logically speaking you would expect any files in projects B or C marked Copy to output directory = Always to be copied to project A's output directory in addition to their project's respective output directory.

I guess this depends on what your definition of "output directory" is. Whether it's the output directory of the referenced project or the output directory of the project doing the referencing. Either way for me, my main release roadblock was removed.

Something to keep in mind should you be moving to VS2012/.net Framework 4.5.

No comments: