Jul 30, 2007

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\(&quot;.*&quot;\)\]"
ReplacementText="AssemblyVersion(&quot;$(Major).$(Minor).$(Build).$(Revision)&quot;)]" />
</
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.

10 comments:

  1. Since TfsVersion interrogates the build server's workspace, and BuildNumberOverrideTarget comes early in the process...you might find that you don't get the changeset number you expect. See my "update" to this post: http://www.traceofthought.net/2008/01/02/CustomizingTFSTeamBuildBuildNumbers.aspx

    ReplyDelete
  2. What's the main point of assembly and build number synchronization if new AssemblyInfos.cs files haven't been checkedIn in TFS? Everyone who will download project after you from TFS will not notice assemblyNumber changes.

    ReplyDelete
  3. You can always check the assemblyinfo files out, change them and then check them back in as part of the build script. Just remember that if you're not careful the check of the version changes could cause another build to be triggered, leading to a continuous build loop.

    In any case I prefer to keep things separate as it means that developer built version numbers are different from the build server built version numbers.

    It's pretty easy to do either way and is more a matter of personal preference than anything else.

    ReplyDelete
  4. Richard - if you include '***NO_CI***' in the comments when you check-in, the check-in will not trigger a CI build (and hence no CI Build loop)

    Until SP1 however, Daily builds will still be triggered regardless of the NO_CI comment...

    ReplyDelete
  5. Sreenivas KothapalliAug 22, 2009, 11:09:00 AM

    Can also create a CommonAssemblyInfo.cs and create a link to it from all the projects. In addition to version, it may have copyright and company information. The AssemblyInfo.cs within each project contain project-specific information like assembly title. This way, you need to change/commit only one file.

    ReplyDelete
  6. I've just upgraded to TFS 2010 and my I'm getting errors because apparently the get operation doesn't work with writable files. The AsssemblyInfos will have been made writable from the Attrib task on the first build so the next build will fail when it's get tries to overwrite the files.

    Adding another <Attrib> after the <FileUpdate> worked for me:

    <Attrib Files="@(AssemblyInfoFiles)" ReadOnly="true" />

    ReplyDelete
  7. how can I add version file where last version number is revision number? (example 1.0.0.2765)

    ReplyDelete
  8. Thanks Richard for posting a solution for TFS2010, although it does not cover my scenario since I am using the upgrade template and not the default template with workflow functionality in the buld definitions. I am not sure if someone is still monitoring this thread anymore, but I'll try. I am upgrading TFS server and build servers to TFS 2010, while still using VS 2008 solutions and clients. The community tasks solution has worked good in TFS 2008, but I can't get it to work in TFS 2010, and I can't find anything on different forums for how to solve this. I am wondering if it is at all possible to get the changeset number appended to the build number in TFS 2010, plus updating the revision of the assemblies.

    I am using the upgrade template and not the default template for the build definitions, and I had to set the build to run MSBuild in 32 bit mode instead of 64.

    I am running out of options and it would be good to know if us users that want to have this type of functionality should even continue trying to get the community tasks to work.

    I am considering setting up a build with the workflow function and default template instead, but that will require re-doing the builds and that is what I have been hoping to avoid. If I go the workflow path, I will run into other issues.

    I looked at other solutions that have been discussed on forums, but have no success in finding any that corresponds to my scenario. Some people talk about MSBuild extention tasks package, but I have not seen anywhere that it would support adding changeset number to the build number.

    ReplyDelete
  9. @sven The build extensions are on codeplex at http://tfsbuildextensions.codeplex.com/

    What might be happening is that the extensions are using the TFS2008 API calls to work out the version information (though I haven't checked - it's just a guess). It might be worth installing the 2010 forward compatibility patch on the build server and seeing if that helps.

    ReplyDelete
  10. @sven Oops - the link the for MSBUILD extensions (not the TFS ones - doh!) is http://msbuildextensionpack.codeplex.com/

    ReplyDelete