I've been meaning to automate some if not all of the build on the project I work on for some time. Initially I considered NANT, because I have previous experience with ANT, and MSBuild. After a very small amount of research I decided that MSBuild was the way to go. It's really a no brainer. NANT is no longer developed, whereas MSBuild is, and seems very similar, plus custom tasks can be written.
At minimum I wanted to set up an automated build that:
- Built the project in a specified configuration
- Published the project
- Set the release version in the web.config
- Zipped the published files
- Transferred zip file to the server
Ideally I'd like to zip and deploy the files, unfortunately due to our appalling network connection unzipping over the network is very slow and doesn't save time, so I gave up on that idea. In addition to the above steps I wanted the process to be configurable for each developer without changing the MSBuild.xml file. For example it's likely that each developer will have slightly different paths to the project on their machine. The situation I wanted to avoid was having each developer change a path in the MSBuild.xml file and overwrite someone else's file when they did a checkout from source control.
Set up
The first thing to do is add the MSBuild.exe to your PATH environment variable. Typically the path will be something like: C:\Windows\Microsoft.NET\Framework\v3.5\MSBuild.exe.
Download the community tasks if you think you'll need them (you almost certainly will): http://msbuildtasks.tigris.org/.
Create an MSBuild.xml file in your project directory. The bare bones file looks like this:
<Project DefaultTargets="SomeDefaultTarget" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="SomeDefaultTarget">
<!-- Do some stuff here -->
</Target>
</Project>
Actions are carried out by creating <Target> tags. Each <Target> tag contains one or more tasks like <MSBuild>, or <Copy> which are generally fairly self explanatory. Other important tags you'll need are <PropertyGroup>, for defining reusable properties; <ItemGroup>, for defined things like file groups; and <Import> for importing custom tasks. I'm not going to explain how these tags work, the documentation is pretty good, my aim in this blog is to describe how I overcame the particular problems I faced.
Publishing
One of the first problems I had to figure out was how to publish the files as you don't want all the files in your project to be in your web site. Turns out it's pretty simple. Just a case of specifying the OutDir property in the MSBuild task.
<MSBuild Projects="$(ProjectRootPath)\MyProject.vbproj"
Targets="_CopyWebApplication;_BuiltWebOutputGroupOutput"
Properties="OutDir=$(BuildFilesPath)\" />
$(ProjectRootPath) and $(BuildFilesPath) are properties I've created. The files are published in a directory called "_PublishedWebsites" within $(BuildFilesPath).
Setting the version
The version number of the software is specified as an application setting within the web.config in the particularly project I work on. Fortunately the community tasks download provides an XmlUpdate task:
<XmlUpdate XmlFileName="$(PublishPath)\web.config"
XPath="/configuration/appSettings/add[@key='version']/@value"
Value="$(ProjectVersion)" />
Working out the required XPath string is the hardest aspect of this tag, and that isn't very difficult really. The above example finds the <configuration> tag containing the <appSettings> tag containing an <add> tag with an attribute called "key" with a value of "version", and selects the "value" attribute. The "value" attribute is set to the value specified by the $(ProjectVersion) property.
Making it configurable
Something I wanted to avoid was storing developer specific paths in the MSBuild.xml file. Obviously the build file should be in source control, but you don't want to be in a situation where developers are constantly overwriting each others build paths when checking out from or committing to source control. To avoid this all developer specific variables must be stored in configuration file which is not in the repository.
After a little research and a number of false starts I found that a good way to create a configuration file was actually to use another build file and import it. So, simply create a file containing the required configurable properties and give it a name, for example MSBuild.props:
<Project ToolsVersion="4.0" DefaultTargets="Default"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectRootPath>C:\PathToProject</ProjectRootPath>
<ExtensionsPath>C:\PathTo\MSBuild</ExtensionsPath>
</PropertyGroup>
</Project>
Now it is simply a case of importing the properties into your main build file using the import tag:
<Import Project="MSBuild.props" />
I've not included a path in this example because I'm assuming that the MSBuild.xml and MSBuild.props are in the same directory. Once imported you can use the properties as normal. Put your import tag at the top of the MSBuild.xml file otherwise you run the risk of using properties before they have been initialised. I only mention it because it's a mistake I made. Whoops.
Running the build
In addition to a properties file I also wanted to specify options at build time, namely configuration (e.g. release) and project version. Ideally these options should be inputed via a menu. I decide to do this using a batch file. I would recommend using another scripting language if you know one. I don't, and I knew I could knock up a batch file quickly, so that's why I chose a batch file. My build file takes a configuration and a project version then calls MSBuild.
echo OFF
:MENU
cls
echo 1 - Staging
echo 2 - Training
echo 3 - Preview
echo 4 - Live
echo 5 - Exit
echo.
set /P M=Choose:
if %M%==1 set config=Staging
if %M%==2 set config=Training
if %M%==3 set config=Preview
if %M%==4 set config=Release
if %M%==5 exit
set /P projectVersion=Project version [ENTER to remain unchanged]:
msbuild MSBuild.xml /p:Configuration=%config% /p:ProjectVersion=%projectVersion%
pause
The above file displays a menu allowing the using to select the build configuration or exit (please note the configurations a specific to this particular project). Once selected the user then enters the project version, hits enter and the build starts. I've added the pause command at the end because typically you would set up a short cut to the batch file and without it the command window would disappear after the build was complete, hiding any success or error messages. Take note of the msbuild command. It takes the MSBuild file as the first parameter, then I've passed in two properties. Using /p: is a shortcut for /property. Predefined properties, like Configuration, or user defined properties, like ProjectVersion, can be set this way.
Useful links