2010-04-10

Custom targets for solutions and projects in MSBuild

By stumbling upon a couple of lines in Microsoft.Common.targets and then reading a very good blog post about extending MS Build, I learned a lot about how one could inject own behavior into MSBuild by using the conditional import mechanism found in Microsoft.Common.targets. You create two MSBuild files and you put them into a folder named by the version number of MSBuild (e.g. v3.5) under whatever $(MSBuildExtensionsPath) point to in your installation of MSBuild. One is called Custom.Before.Microsoft.Common.targets and will be imported before all other definitions in Microsoft.Common.targets. The other is named Custom.After.Microsoft.Common.targets and will be imported after all definitions. All this is pretty well documented already.

Here is the twist: By adding import statements that looks for target files in specific directories and following a naming pattern, every project can have its own MSBuild files that are imported before and after the main definitions in the Microsoft.Common.targets file.

The result is that every project in a solution will build the exactly way you want them to. Even when you build your solution inside Visual Studio! The solution targets files will affect every project, while the project targets file will only affect the specific project.

There are a number of things one can do with this mechanism. Here are some examples:

  1. Change the way version numbers are updated when building. How about leaving AssemblyInfo.cs alone and let a build task update the AssemblyVersion and AssemblyFileVersion attributes automatically, by e.g. picking up $(CCNetLabel) set by CruiseControl.Net
  2. Change the $(AssemblySearchPaths) property to have a more restrictive resolving of references in a build. We don't want Visual Studio to go looking for assemblies to "help us"
  3. Suppress those annoying "missing xml comments" warnings that no-one cares about anyway.
  4. Inserting targets into $(BuildDependsOn) that are called before or after build, instead of using those nasty pre- and post-build steps that you specify in the project properties dialogue.

Custom.Before.Microsoft.Common.targets:

Custom.After.Microsoft.Common.targets:

Download the source here

So why bother implementing a practice like this instead of just putting all your custom behavior into the custom target files themselves? Well, by doing that, you will affect all kinds of projects. By importing solution and project level MSBuild files if they are there, the projects and solutions themselves can decide exactly how they want to be built.

A couple of caveats:

  1. If you mess up your solution and project level MSBuild files, you may get your VS rendered useless until you fix the problems. The only way is to edit your files without loading the solution and test it with MSBuild at the VS command prompt.
  2. If you edit your solution and project build files from within your solution in VS, then you have to close the solution and reopen it to get them applied. I guess VS caches the build definitions to be able to resolve references, etc.
If you are not working as a solo developer, you may want to create a little MSBuild file that installs your source controlled custom target files into $(MSBuildExtensionsPath)\v3.5\ or whatever your version of MSBuild is. Then you make sure that it is run regularly at build servers and developer laptops. This way, you can change your custom before and after files and be confident that all developer and build environments are up to date (or destroyed if you make a mistake...)