Versioning Builds with TFS and MSBuild
UPDATE: A post showing how this works in TFS 2010 is now available
In this post I want to show you one way to add a version file to a web site project and a version number to a business layer DLL based on the latest changeset number for your code in TFS, all through a single MSBuild script.
If you want to do this in your own projects you'll need to make sure you have MSBuild (part of Visual Studio 2005) and that you have also obtained the latest MSBuild Community Tasks.
Out of the box MSBuild includes enough tasks to cover the needs of building applications within Visual Studio 2005 but in order to really make it sing we need to extend its functionality through the use of custom tasks. Now if we wanted we could write our own tasks, but why reinvent the wheel? The MSBuild Community Tasks are a great set of tasks and provide all the extra features we need to achieve our goal. Oh, by the way, the web site for the tasks is pretty much a placeholder - all the real information on the tasks is available in a CHM file that comes with the install kit.
Now, what we want our build script to do is the following:
1. Get the latest Changeset number from TFS. We'll use this as the revision number. We want to end up with an assembly version number like 1.2.3.### with ### being the changeset number.
2. Update all the AssemblyInfo files with the desired version number.
3. Compile the application.
4. Add a version.txt file to our web site so that we can see what build version the web site is.
OK, let's get started!
Script Header
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>
This defines the project, set’s the default target to "Build" and imports the community task definitions, ready for later use.
Properties
<PropertyGroup>
<Major Condition="'$(Major)'==''">1</Major>
<Minor Condition="'$(Minor)'==''">0</Minor>
<Build Condition="'$(Build)'==''">0</Build>
<Revision Condition="'$(Revision)'==''">0</Revision>
<Configuration Condition="'$(Configuration)'==''">Debug</Configuration>
</PropertyGroup>
Here we define the various properties we will use in the project. We're setting up 4 properties to hold the 4 parts of the version number, and we're also creating a property for the build configuration we wish to use (ie Debug or Release). By default our version number will be 1.0.0.0 and we'll be building the application in Debug mode
Properties in MSBuild are referenced using the $(PropertyName) syntax and any properties not explicitly defined will be evaluated as empty strings.
The Condition clause is used to determine if a property has a supplied value and if it doesn’t then we supply a default value.
When MSBuild gets called from the command line the value of the /p: switch is parsed to populate the property values with initial values.
Item Groups
<ItemGroup>
<ProjectsToBuild Include="BusinessLayer.csproj" />
<ProjectsToBuild Include="MyWebSite.sln" />
</ItemGroup>
<ItemGroup>
<AssemblyInfoFiles Include="$(MSBuildProjectDirectory)\**\assemblyinfo.cs" />
</ItemGroup>
We now define Item Groups. Item Groups are conceptually the same as collections and contain "items" that the various MSBuild tasks can act upon. The Include clause allows us to add multiple items to the collection in one go.
Here we're creating a collection of projects to build - the business layer and the web site itself.
We also create a collection of assemblyinfo.cs files. The double asterix (**) on the AssemblyInfoFiles element ensures that all subdirectories are recursively searched for the assemblyinfo.cs files, regardless of their depth.
ItemGroups are referenced in MSBuild using the @(ItemGroup) syntax.
Main Build Target
<Target Name="Build" DependsOnTargets="SetVersionInfo;SetWebVersionInfo">
<MSBuild Projects="@(ProjectsToBuild)" Properties="Configuration=$(Configuration)" />
</Target>
Targets are the items that define the work MSBuild will perform. Here we define the default target for the build.
Hang on. Why are we building now - we haven't done anything about the version number... Notice the DependsOnTargets property? MSBuild will ensure that any targets listed there are evaluated and processed before this target gets actioned.
This means we will actually process SetVersionInfo before we recursively call MSBuild to compile the business layer and the web site.
When the MSBuild task gets called it will process the projects in the order they exist in the ProjectsToBuild item group.Note that we are passing through the configuration we to build.
SetVersionInfo Target
<Target Name="SetVersionInfo" DependsOnTargets="GetTFSVersion">
<Attrib Files="@(AssemblyInfoFiles)" Normal="true" />
<FileUpdate Files="@(AssemblyInfoFiles)"
Regex="AssemblyVersion\(".*"\)\]"
ReplacementText="AssemblyVersion("$(Major).$(Minor).$(Build).$(Revision)")]" />
</Target>
Here we process all the AssemblyInfo.cs files and set them to have a specific version number based on the property values we have defined.
First we clear the ReadOnly attribute on the files. Why? Because TFS sets this attribute when it retrieves the code from source control and unless you have the files checked out they will be read only.
We then do a search and replace of the AssemblyVersion values in the AssemblyInfo.cs files using regular expression. We replace the existing code with the specific version we want using the Major, Minor, Build and Revision properties.
But where does the Revision number come from? That’s in the GetTFSVersion target.
GetTFSVersion Target
<Target Name="GetTFSVersion">
<TfsVersion LocalPath="$(CCNetWorkingDirectory)">
<Output TaskParameter="Changeset" PropertyName="Revision"/>
</TfsVersion>
<Message Text="TFS ChangeSet: $(Revision)" />
</Target>
This target queries TFS using the TfsVersion tasks to get the latest Changeset number and places this value in the Revision property.
MSBuild has a weird syntax for getting return values from tasks. The Output element shows how it works. The TfsVersion tasks has a parameter called Changeset and when the task completes the parameter value will have a value. We can access this value using the Output element and assign it to a property. It's a bit like a C# Property Get in concept but it's not a very elegant syntax.
We’re also producing a message in the build log for reference (just to show we can).
SetWebVersionInfo Target
The last thing we need to do is to add a version file to the web site we are going to compile.
<Target Name="SetWebVersionInfo">
<Version VersionFile="MyWebSite\version.txt" BuildType="None" RevisionType="None" Major="$(Major)" Minor="$(Minor)" Build="$(Build)" Revision="$(Revision)" />
</Target>
Here we're just creating a simple version.txt file in a hardcoded location. Remember that there's nothing stopping you from using MSBuild properties to control the location of the version file, or doing something along the lines of what we did with the AssemblyInfo.cs files by putting the version information in a resource file or editing an "about.htm" page.
After we've done this, we just need to remember to close of the <project> tag, save the build file and we're done.
Try It
Give it a run my calling MSBuild from the command line using a statement like
MSBuild MyBuildScript.proj /p:Configuration=Debug;Major=2;Minor=4;Build=1
Normally you'd want to do this as part of a CI process using Team Build and TFS Integrator or CruiseControl.NET, or any other CI product that allows you to execute MSBuild tasks.
For more information on using CruiseControl.NET with TFS see my post on this subject.
Information on MSBuild is available from MSDN. Good starting points are the MSBuild Overview and the MSBuild Reference.