A couple of months ago Shannon wrote a post on how we do 2 Click Deployments at The Farm (if you haven’t read this post I suggest you read it first as I make the assumption it’s been read and skip over a few sections that it covers). I’ve been working with him to try and improve our process of deployment, and one of those tasks has been moving away from using NAnt as the build runner to using MSBuild.
Why the shift? Well MSBuild has actually come a long way recently and despite common belief it’s not just for building .NET applications. In fact it can be used the same way that NAnt can be used, to execute arbitrary operations.
As mentioned in the original post we’ve used an internal tool for modifying the web.config (and other configuration files) but it had a bit a limitation as it meant that the files were always full of settings for all environments. It also wasn’t designed to update a single property of an XML node, you’d need to replicate the whole XML structure, even if it was just 1 attribute changing.
So I suggested that we migrate to a XSLT based config management, which is what I’d used previously. A former colleague of mine has a good post on how to set that up, it can be found here.
Moving to MSBuild also brought in another advantage, it would simplify our deployment process. Currently we’re using CruiseControl.NET to execute a NAnt script which in turn executed MSBuild.
Another goal was to allow us to deploy from the branch in source control, rather than from the trunk, this way you can deploy different branches if you want to test different functionality based on a branch without overriding the trunk. So we’ve updated our source control structure to look like this:
The new addition is the build folder, this folder will generally only hold a single file, and that’s the master build information, which we’ll call cc.build. This build file is the one that CruiseControl.NET will look for and check out. It then contains the instructions to which branch to check out, and then handle the rest of the execution of the build. A sample looks like this:
<Project ToolsVersion="3.5" DefaultTargets="CheckoutAndBuild" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>
<PropertyGroup>
<BranchVersion>v1.1_net</BranchVersion>
<BuildType>_net</BuildType>
<BranchFolder>$(BaseFolder)\$(BuildType)</BranchFolder>
<RepoPath>file:///\svn/Client/branches/$(BranchVersion)</RepoPath>
</PropertyGroup>
<Target Name="CheckoutAndBuild">
<RemoveDir Directories="$(BranchFolder)" />
<SvnCheckout RepositoryPath="$(RepoPath)"
LocalPath="$(BranchFolder)"
ToolPath="C:\Program Files\CollabNet Subversion">
<Output TaskParameter="Revision" PropertyName="Revision" />
</SvnCheckout>
<Message Text="Revision: $(Revision)"/>
<Time Format="yyyyMMddtt">
<Output TaskParameter="FormattedTime" PropertyName="buildDate" />
</Time>
<MSBuild Projects="$(BranchFolder)\cc.msbuild"
Properties="CCNetWorkingDirectory=$(BaseFolder);BuildDate=$(BuildDate);DeployType=Dev;DeployLocation=$(BaseFolder)..\Deployment\Dev;BuildConfiguration=Debug;ShowMessages=True;IncludeSymbols=True" />
</Target>
</Project>
So lets have a look at the break down of this file. First off, we’re using the MSBuild Community Tasks for tasks such as the SVN checkout, and a few other aspects which we’ll cover later.
When we call this MSBuild file from CruiseControl.NET and pass in a parameter, BaseFolder, which is the Working folder (see the Folder Structure section of the original post).
There’s some parameters combined so we get a path to where we’ll be checking out the branch, etc.
First thing is to remove any existing checkout folder, and then have SVN checkout the latest copy of the branch.
As Shannon mentioned in the original post we timestamp each sip package, so for this we’re using the Community Task date formatter to generate our timestamp.
Then it gets a bit tricky, because this file is a dumb file and doesn’t know what’s going to be done by the branch deployment file so we need to have a way in which we can actually do a build. This is done by using the MSBuild task which allows you to execute any specified MSBuild file(s). This is done by passing the file to execute into the Projects attribute. Since we’re going to execute the one within the branch we tell it to look there.
Then it’s just a matter of passing any properties you require into the MSBuild file, here we’ve got a bunch of different parameters which the target file can consume.
The MSBuild task is then replicated as many times for each different environment/ location which you want to build for.
Each branch maintains its own msbuild file, which is also called cc.msbuild so that we can keep the naming consistent across all locations. This file is the one which is responsible for:
- Compiling the project
- Modifying the config files
- Zipping the release
But this can easily be expanded upon, depending how the branch needs to be handled, there’s nothing that says it couldn’t also do the copying to the appropriate server, etc. Lets take a look inside this file.
<Project ToolsVersion="3.5" DefaultTargets="ZipFiles" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>
<PropertyGroup>
<LocationWorkingWeb>$(CCNetWorkingDirectory)\_net\Client.Web</LocationWorkingWeb>
<ProjectWeb>$(LocationWorkingWeb)\Client.Web.csproj</ProjectWeb>
<BuildFolder>$(DeployLocation)\$(BuildDate)\</BuildFolder>
</PropertyGroup>
<Target Name="ZipFiles" DependsOnTargets="FormatFiles">
<ItemGroup>
<ZipFiles Include="$(BuildFolder)**\*.*" Condition="$(IncludeSymbols) == 'True'" />
<ZipFiles Include="$(BuildFolder)**\*.*" Exclude="$(BuildFolder)**\*.pdb" Condition="$(IncludeSymbols) == 'False'" />
</ItemGroup>
<Zip Files="@(ZipFiles)"
ZipFileName="$(DeployLocation)\$(BuildDate).zip"
WorkingDirectory="$(BuildFolder)"/>
<RemoveDir Directories="$(BuildFolder)" />
</Target>
<Target Name="FormatFiles" DependsOnTargets="BuildWebProject">
<MSBuild Projects="transformers.msbuild"
Properties="XslFile=$(CCNetWorkingDirectory)\_net\CruiseControl\Web.config.xslt;OutputFile=$(BuildFolder)Web.config;InputFile=$(LocationWorkingWeb)\Web.config;Environment=$(DeployType)" />
</Target>
<Target Name="BuildWebProject">
<MSBuild Projects="$(ProjectWeb)"
Properties="Configuration=$(BuildConfiguration);OutDir=$(BuildFolder)bin\;WebProjectOutputDir=$(BuildFolder)"
Targets="Clean;Build;ResolveReferences;_CopyWebApplication"/>
</Target>
</Project>
This file, again uses the MSBuild Community Tasks, but its also a whole lot smarter about what’s happening for this particular build. I’ve specified that the DefaultTargets of the file is the ZipFiles which is really the final task which I want to execute.
One really nice thing about MSBuild is that you can specify a DependsOnTargets attribute for a target, which states that the particular target(s) must have executed before the called one will execute. So by looking at how the dependency is configured the project will have been compiled and the files formatted will both have been done. Makes it so much simpler to execute something, rather than having a ‘runner’ target which is just responsible for calling a bunch of targets in order we can just have them done in order of dependency.
So we’ve got a BuildWebProject target which will again call an external MSBuild file, in this case our csproj file, passing in some of the information which was given to us from the ‘master’ file, in a manner in which Visual Studio itself would have done.
Next we format the files. This goes off to an external MSBuild file, so we can use the same operations for every single file we want to format. The contents of transformers.msbuild is as follows:
<!-- ROBOTS IN DISGUISE -->
<Project ToolsVersion="3.5" DefaultTargets="Xslt" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>
<ItemGroup>
<XslFileWithParams Include="$(XslFile)">
<environment>$(Environment)</environment>
</XslFileWithParams>
</ItemGroup>
<Target Name="Xslt">
<Xslt RootTag="" Inputs="$(InputFile)" Output="$(OutputFile)" Xsl="@(XslFileWithParams)" />
</Target>
</Project>
This is a basic task file which adds a parameter (named envrionments) to our XSLT and then uses the MSBuild Community Tasks to do the transformation.
Note – this is a slight deviation from Alistair’s blog post, we don’t have a different XSLT per machine, we have a single XSLT which we use the parameter to determine what to do. This makes it easier to see all the options we want to change per environment.
Lastly the dependencies hierarchy throws us back to the ZipFiles target, where we gather all the files together that we want (and there’s an option to exclude the pdb files, after all PDB != Product Deployable Bits ;) ), call the Zip task (make sure you specify a working folder otherwise you’ll end up with a crazy hierarchy in the Zip!) and then we delete the folder which we built to.
Conclusion
MSBuild is a good tool which can be used for doing operations other than just compiling a .NET project, as you can see here we’re using it to pull files down out of SVN or transform a file via XSLT. This doesn’t have anything to do with compilation, or .NET for that matter.
It make take a bit of work to get set up, there was quit a bit of frustration vented during this process but now that it’s working there’s no looking back.