Practical MSBuild - Flexible Configuration

Any sufficiently complex build script will contain dozens of variables. Database names, IIS sites, compile options and test configurations are good examples of variable that can change from machine to machine, even branch to branch.

Many scripts I've seen rely on various assumptions about the machine it will be running. Others force everything to be explicitly defined for every build run. Many try to do both (TFS) at once and result in a spaghetti like mess.

This is what I have found to be an effective approach to juggling these variables, avoiding unnecessary surprises yet still remaining flexible.

Declaring the Properties

First of all, a build script should declare all it's variables near the top. These values should generally be sensible defaults, values that will work for most developers most of the time. I'll get to what the condition attributes are for further down, here is a typical PropertyGroup section:

<PropertyGroup>
  <databaseConnection Condition="'$(databaseConnection)' == ''">Data Source=myServerAddress;Initial Catalog=myDataBase;Integrated Security=SSPI;</databaseConnection>
  <nunitPath Condition="'$(nunitPath)' == ''">c:\nunit</nunitPath>
  <projectVersion Condition="'$(projectVersion)' == ''">1.1.0.0</projectVersion>
</PropertyGroup>

Next is the echo task. This will probably seem repetitive but we will be overriding these variables from various sources so it's important to have a sanity check, especially when it's running on a build server. The code itself is straight forward:

<Target Name="echo">
  <Message Text="databaseConnection: $(databaseConnection)" />
  <Message Text="nunitPath: $(nunitPath)" />
  <Message Text="projectVersion: $(projectVersion)" />
</Target>

To run this script open up the Visual Studio command prompt, cd to the project directory and type:

msbuild project.build /target:echo

There are going to be many times when we want to override the default values and this is exactly what the Condition attribute is for. If someone (usually the build server) needed to use a different database it can be overridden from the command line:

msbuild project.build /target:echo /property:databaseConnection="a custom connection string"

databaseConnection will only be defined if the Condition attribute evaluates to true. Because we defined the property from the command line the condition evaluated to false. This simply bit of code has created a precedence order where command line property > default property. In real world usage this saves a great deal of automagic configuration and/or assumptions about the operating environment.

Developer Configuration

Of course a real world build script could have dozens of properties and typing these into the command line every time would get a bit monotonous. This can be avoided by creating a properties file that will save all our personal settings.

A properties file (I usually call it local.properties) sits in the root directory (along with the main build file) and is just a special MS Build script the will be imported into the main script. A properties file should never be kept in source control and should be explicitly excluded. Here is what a properties file will look like:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <nunitPath Condition="'$(nunitPath)' == ''">C:\Program Files\nunit</nunitPath>
  </PropertyGroup>
</Project>

The first line of the main build script will then have:

<Import Project="local.properties" Condition="Exists('local.properties')" />

This will import the properties file if it exists and the properties declared in this file will override those in the main script. In this case the properties file overrides the nunitPath property because this developer installed it to a location that differs from the rest of the team, this is no big deal because he only has to specify it once and never think about it again.

Now the precedence order is: command line property > properties file property > default property. This is a good rule that will generally suite everyone. A developer with all the defaults will have no configuration yet configuration will be easy for anyone with different needs.

And that's it, the basis of a flexible, configurable and maintainable build system with MS Build. I intend to write a series of articles on MS Build in the near future as well as a working sample project, as I did with the Frictionless WCF example, so stay tuned.