Edit: 09/11/2011: This entry still describes how the build numbers work, but I’ve updated the files and shown how to incorporate these changes into .CSPROJ and .VCXPROJ files, Read the follow on blog entries, http://www.wintellect.com/CS/blogs/jrobbins/archive/2011/09/05/tfs-2010-build-numbers-file-versions-from-inside-your-c-and-c-projects.aspx and http://www.wintellect.com/CS/blogs/jrobbins/archive/2011/09/11/more-on-tfs-2010-build-numbers-inside-your-projects.aspx

Obviously, based on all the web links out there, keeping your TFS 2008 build number and your assembly version numbers in sync is a pretty hot topic. As others I worked with had always taken care of the TFS build, I never really looked much at what was going on, but all those web links talk about custom build tasks, assemblies, and installing gave me the impression it was kind of a. As I’m moving my entire life over to TFS 2010 Beta 2, it was time for me to look at the doing my own builds from scratch so I could learn more about Team Build. Of course, the first think I needed was a task to keep the TFS build number and my file versions in sync.

With TFS 2010 now based on Work Flow, I was wondering if things were different, and they certainly are. One approach is to base your build off the UpgradeTemplate.XAML and you in essence are sticking with the TFS 2008 approach, which is MSBuild for everything. That will work, but you are completely missing out. Just some small features in your build of automatic symbol and source server population, automatic test running, and all the other beautiful stuff we get for completely free. Therefore, I was determined to do my builds with the DefaultTemplate.XAML Work Flow. While I could probably have hammered in one of the TFS 2008 build task that people have contributed, I took a step back and wondered if there was an easier way.

Fortunately for us, there is. It’s called MSBuild 4.0! My first thought was I could just whip up a spiffy new inline task , which allows you to shove .NET code inside a task and MSBuild will compile and run it. That’s a great approach, but in order to get the build information I was going to have to use the TFS Build API to access the BuildUri and the TFS Collection to get the data I wanted. That was going to be a good bit of code I was going to have to write. While inline tasks would have solved the problem, I started getting an idea that there was an even simpler approach.

Poking around on my computer, I noticed in C:Program Files (x86) MSBuildMicrosoftVisualStudioTeamBuild is a file Microsoft.TeamFoundation.Build.targets, which contains MSBuild tasks used by the TFS 2008-like approach to your Team Builds. Looking at the file, I saw a task, InitializeBuildProperties, which does exactly what the task I was going to have to write needs to do. That got me wondering if I could call that task during a Team Build to get the key BuildNumber property for me. Tossing together a quick build script, I confirmed calling the InitializeBuildProperties task does not screw up Team Build or your build.

About 30 minutes of poking around later, I realized that with some additional advances in MSBuild 4.0, everything turned out to be massively easier than I would have ever guessed. I was able to solve the TFS build number and file version issue with only MSBuild code. By writing everything in straight MSBuild that means no dependencies except for what is already on a TFS Build Server. The fewer dependencies you have, the better off everyone is.

In the download are example showing how to integrate version number file creation into .CSPROJ and .VCXPROJ files directlyare two files, Wintellect.TFSBuildNumber.Targets and CreateVersionFiles.Proj. The .Targets file, which I commented heavily so you can see how it works, does all the heavy lifting. When working on my idea, I read Mike Fourie’s excellent blog entry on Versioning Code in TFS – Revisited. As Mike is a TFS MVP and a ninja level MSBuild master (he’s one of the people behind the awe inspiring MSBuild Extension Pack), I wanted to make sure my approach followed his best practices recommendations.

As Mike points out, you never want to check in your version files as it will cause you all sorts of major hurt. Having experienced that mistake on projects in the past, I made sure my implementation does not rely on version control at all. Now you’re probably wondering how you can build your code if files don’t actually exist!

The idea is that you’ll use CreateVersionFiles.Proj as an example and customize it for your needs. In your TFS Build Definition, you will ensure that your target to create the version files is the first thing run so you create them before any other part of your build needs them. That seems normal, but you’re now scared for those builds you do on the desktop. Are you going to have all your co-workers screaming at you because of broken builds because of missing files?

Don’t worry I have your back there as well. You’ll just set up your developer builds to use your CreateVersionFiles.Proj as well. You add it to your main batch file or add the targets direction to the <Target Name=”BeforeBuild”> section of a .CSPROJ file. It’s all just standard MSBuild stuff.

If you’re building on a development machine, and your version files are not in version control, what version numbers will you be using? The targets in Wintellect.TFSBuildNumber.Targets have two modes. If they detect they are running under TFS Build, they use the exact build and revision information from TFS. On a local build, they do the following:

  • The build number will use the current date
  • The revision number is set to 65535. (If you are doing more than 65,535 builds on a project each day, you have bigger problems than version files to work on. <grin> Also, 65535 is the highest number that can be in a file version.)
  • If the version file already exists, the targets will not overwrite it thus avoiding affecting your local builds unnecessarily. On a TFS Build, my targets always write the TFS information in case your build definition does incremental gets from version control.

I chose this approach for local builds because the local build file versions don’t matter that much and it’s a reasonable approach. If you want to do more, you have the code and as you’ll see it’s quite easy to change.

The interesting work in Wintellect.TFSBuildNumber.Targets is in the TFSBuildFileVersion target for handling a TFS build. With the TFS InitializeBuildProperties task doing the big work, I just had to concentrate on parsing the string in that property. If your build definition uses a Build Number Format of “$(BuildDefinitionName)_$(Date:yyyyMMdd)$(Rev:.r)”, which my code assumes you are, the BuildNumber property will look like “Dev Branch Daily Build_20091108.14” when filled out. The date and revision information is in there, you just need to do some parsing.

Before MSBuild 4.0, the term “do some parsing” meant, “write a custom task in C# and all the fun that entails with installation, versioning, etc.” What we can do now is take advantage of the amazing new Property Functions in MSBuild 4.0. As all your properties are strings, MSBuild allows you to call String property functions on them. For example, if you wanted to first three characters of a path so you could get the root drive, the following would set the $(RootDrive) value to “C:”.

<RootDrive>$(ProjectOutputFolder.Substring(0,3))</RootDrive>

Additionally, the property functions also include static methods, such as DateTime.Now and special built in property functions, which start with [MSBuild], that allow for all sorts of arithmetic operations. Armed with those, parsing a string is like a hot knife through butter!

Below is the TFSBuildFileVersion that shows all the parsing and work to pull out the build date and revision out of the TFS BuildNumber property. Note that the version number I build up properly accounts for file versioning as build numbers cannot be greater than 65535 as I mentioned previously. My scheme is the same scheme used by the Visual Studio team so the first 10.0 build on October 6, 2009 (any idea why that date is important?) produces a file version of 10.21006.1, where 1 is the TFS revision value.

<Target Name=TFSBuildFileVersion
    DependsOnTargets=$(DependOnGetBuildProperties)>
    <!– Do the error checking to ensure the appropriate items are defined.–>
    <Error Condition=‘$(TFSMajorBuildNumber)’==”
                 Text=TFSMajorBuildNumber is not defined./>
    <Error Condition=‘$(TFSMinorBuildNumber)’==”
                 Text=TFSMinorBuildNumber is not defined./>
    <PropertyGroup>
    <!– The separator string between the $(BuildDefinition) and the date
     revision.–>
        <BuildDefSeparatorValue>_</BuildDefSeparatorValue>
        <!– The separator between the date and revision.–>
        <DateVerSeparatorValue>.</DateVerSeparatorValue>
    </PropertyGroup>

    <!– The calculations when run on a TFS Build Server.–>
    <PropertyGroup Condition=‘$(WintellectBuildType)’==’TFSBUILD’>
        <!– Get where the timestamp starts–>
        <tmpStartPosition>$([MSBuild]::Add($(BuildDefinitionName.Length), $(BuildDefSeparatorValue.Length)))</tmpStartPosition>
        <!– Get the date and version portion. ex: 20091107.14–>
        <tmpFullDateAndVersion>$(BuildNumber.Substring($(tmpStartPosition)))</tmpFullDateAndVersion>
        <!– Find the position where the date and version separator
        splits the string.
–>
        <tmpDateVerSepPos>$(tmpFullDateAndVersion.IndexOf($(DateVerSeparatorValue)))</tmpDateVerSepPos>
        <!– Grab the date. ex: 20081107–>
        <tmpFullBuildDate>$(tmpFullDateAndVersion.SubString(0,$(tmpDateVerSepPos)))</tmpFullBuildDate>
        <!– Bump past the separator. –>
        <tmpVerStartPos>$([MSBuild]::Add($(tmpDateVerSepPos),1))</tmpVerStartPos>
        <!– Get the revision string. ex: 14–>
        <TFSBuildRevision>$(tmpFullDateAndVersion.SubString($(tmpVerStartPos)))</TFSBuildRevision>
        <!– Get the pieces so if someone wants to customize, they have
            them.
–>
        <TFSBuildYear>$(tmpFullBuildDate.SubString(0,4))</TFSBuildYear>
        <TFSBuildMonth>$(tmpFullBuildDate.SubString(4,2))</TFSBuildMonth>
        <TFSBuildDay>$(tmpFullBuildDate.SubString(6,2))</TFSBuildDay>
    </PropertyGroup>

    <PropertyGroup Condition=‘$(WintellectBuildType)’==’DEVELOPERBUILD’>
        <TFSBuildRevision>65535</TFSBuildRevision>
        <TFSBuildYear>$([System.DateTime]::Now.Year.ToString(“0000”))</TFSBuildYear>
        <TFSBuildMonth>$([System.DateTime]::Now.Month.ToString(“00”))</TFSBuildMonth>
        <TFSBuildDay>$([System.DateTime]::Now.Day.ToString(“00”))</TFSBuildDay>
    </PropertyGroup>

    <PropertyGroup>
        <!– This is the Excel calculation “=MOD(year-2001,6)”–>
        <!– That’s what it looks like DevDiv is using for their
            calculations.
–>
        <TFSCalculatedYear>$([MSBuild]::Modulo($([MSBuild]::Subtract($(TFSBuildYear),2001)),6))</TFSCalculatedYear>

        <TFSBuildNumber>$(TFSCalculatedYear)$(TFSBuildMonth)$(TFSBuildDay)</TFSBuildNumber>

        <TFSFullBuildVersionString>$(TFSMajorBuildNumber).$(TFSMinorBuildNumber).$(TFSBuildNumber).$(TFSBuildRevision)</TFSFullBuildVersionString>
    </PropertyGroup>

    <!– Do some error checking as empty properties screw up everything.–>
    <Error Condition=‘$(TFSFullBuildVersionString)’==”
                 Text=Error building the TFSFullBuildVersionString property/>
    <Error Condition=‘$(TFSBuildNumber)’==”
                 Text=Error building the TFSBuildNumber property/>
    <Error Condition=‘$(TFSCalculatedYear)’==”
                 Text=Error building the TFSCalculatedYear property/>
    <Error Condition=‘$(TFSBuildDay)’==”
                 Text=Error building the TFSBuildDay property/>
    <Error Condition=‘$(TFSBuildMonth)’==”
                 Text=Error building the TFSBuildMonth property/>
    <Error Condition=‘$(TFSBuildYear)’==”
                 Text=Error building the TFSBuildYear property/>
    <Error Condition=‘$(TFSBuildRevision)’==”
                 Text=Error building the TFSBuildRevision property/>
</Target>

That’s great you can get a valid version number in the $(TFSFullBuildVersionString), but what are you going to do with it? I’ve got you covered because there are all sorts of targets in Wintellect.TFSBuildNumber.Targets like the following to create a C# AssemblyVersionAttribute file, to write out the version information to a file ready for your project’s consumption.

<Target Name=WriteSharedCSharpAssemblyVersionFile
    DependsOnTargets=TFSBuildFileVersion
    Condition=(‘$(WintellectBuildType)’==’TFSBUILD’) or
        ((‘$(WintellectBuildType)’==’DEVELOPERBUILD’) and
         (!Exists($(CSharpAssemblyVersionFile))))>
    <ItemGroup>
        <CSharpLines Include=
// &lt;auto-generated/&gt;
// This file auto generated by the Wintellect TFS 2010 Build Number Targets.
using System%3B
using System.Reflection%3B
[assembly:AssemblyFileVersion(&quot;$(TFSFullBuildVersionString)&quot;)]
/>
    </ItemGroup>
    <WriteLinesToFile Overwrite=true
                File=$(CSharpAssemblyVersionFile)
                Lines=@(CSharpLines) />
</Target>

As TFS is still Beta 2, there’s no guarantee that this task will work in the RTM bits, but I like it so I will update as appropriate. It was a fun little task (pun totally intended!) to poke through and get this working. The new MSBuild 4.0 property functions feature is hugely impressive and can see they are going to make everyone’s life much easier. I hope you find the code useful and please don’t hesitate to ask any questions you might have.