John Robbins' Blog

TFS 2010 Build Number and Assembly File Versions: Completely In Sync with Only MSBuild 4.0

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)\ MSBuild\Microsoft\VisualStudio\TeamBuild 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.

On Nov 8 2009 6:57 PMBy jrobbins With 33 Comments

Comments (33)

  1. All,

    I fixed a small bug where I wasn't properly escaping the semicolons when writing out the C# and C++/CLI files. The download link has been updated.

    - John Robbins

  2. Thanks, this is great. At least the versioning aspect of our upgrade will be relatively painless. Now on to remote web and service deployments in the new template mechanism...

  3. ASPNET Expert,

    Yep! Everything still works with the RC and will with RTM.

    Thanks for using it.

    - John Robbins

  4. Found this while looking for information about WiX and TFS 2010, but I feel like I stumbled on a gold mine. Thanks for writing it up!

  5. Hi John.

    I'm new to MSBuild/TFS2010, so could you tell me how I would integrate this into a TFS2010 Build Definition and where $(TFSMajorBuildNumber) and $(TFSMinorBuildNumber) are specified. Will these be passed in from TFS Build?

    Other than those questions I found your article/builds files easy to follow and informative.

    Thanks

    Michael

  6. Michael,

    The $(TFSMajorBuildNumber) and $(TFSMinorBuildNumber) are values you set before including my task. If you're working on 4.0, specify 4 and 0 respectively.

    Hope it helps,
    -John Robbins

  7. Thanks John.

    Where about would I include the "Wintellect.TFSBuildNumber.targets" file and when would I call the "TFSBuildFileVersion" task?

    Many Thanks

    Michael

  8. It would have helped if you had given step by step list of things to do to get this to work. Your post looks intriguing but it is less useful for those who are not hard core TFS or MSbuild users but are just learning.

    Thanks,

  9. Ash,

    How about looking at the download? There's a full example of how to use this in the unit test.

    - John Robbins

  10. Did I miss something? In the download there are only 2 files Wintellect.TFSBuildNumber.Targets and CreateVersionFiles.Proj. Where is the unit test??

  11. Ash,

    CreateVersionFile.Proj is the unit test. It shows how to integrate the Wintellect.TFSBuildNumber.Targets file into a master build file.

    - John Robbins

  12. I tried your solution with a simple dll project on my local machine and it works fine!However, our tfs build definition builds a solution which wraps couple of C# projects. I had hard time finding out the master build file (if exists) to import your targets file. Could you highlight that to me please?thanks.

  13. The download link is not corrected for the semicolon problem. I changed the ; with %3B wherever required. For building from visualstudio solution are you advocating to add the generated SharedAssemblyFileVersion.cs from a common location as a link in all projects in the solution. And then call it in "BeforeBuild" of each project as shown below&lt;Target Name="BeforeBuild"&gt; &lt;MSBuild Projects="$(MSBuildProjectDirectory)..\Build\CreateVersionFiles.Proj"/&gt; &lt;/Target&gt;This workd in my PC but gives error in TFS build.When building on TFS (TFS, BuildController and BuildAgent installed on same machine) I got errorC:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\TeamBuild\Microsoft.TeamFoundation.Build.targets(359,5): error MSB4062: The "Microsoft.TeamFoundation.Build.Tasks.GetBuildProperties" task could not be loaded from the assembly C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\TeamBuild\..\IDE\PrivateAssemblies\\Microsoft.TeamFoundation.Build.ProcessComponents.dll. Could not load file or assembly 'file:///C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\IDE\PrivateAssemblies\Microsoft.TeamFoundation.Build.ProcessComponents.dll' or one of its dependencies. The system cannot find the file specified. I can find the dll in 'C:\Program Files\Microsoft Team Foundation Server 2010\Tools' but that is not what the targets file expects.Can you help.

  14. hi,
    I understand what it is doing, but strugling how to use in my solution, like where to put the target file and then where to put the proj file etc.
    Regards,
    Sardar

  15. Rich Alexander

    Hi Pratik,I ran into the same error that you had with MSB4062. When I opened the Microsoft.TeamFoundation.Build.targets file, I noticed to my suprise that it was still the TeamBuild 2008 / version 2!My build machine was running TeamBuild 2008. When 2010 came out, I upgraded to TeamBuild 2010 and configured the machine as both a build agent and a controller. It appears that the installation / configuration of TFS 2010 Build Services in this specfic upgrade scenario is leaving behind the older version of the Microsoft.TeamFoundation.Build.targets file.I simply copied over the proper version 3 Microsoft.TeamFoundation.Build.targets file from a working build agent and viola everything is now working.Hope that helps someone out,Rich

  16. Hi John, Pratik,I've run into the same problem that Pratik has. My build agent isn't on my Tfs server; instead, it's simply a VM with nothing more than the build controller and agent installed on it. The symptoms I've experienced are exactly the same as Pratik's--my team build dlls are not where Wintellect.TFSBuildNumber.targets expects them to be, but they are indeed exactly where Pratik found them, too.I suspect this is because neither Pratik nor I had Vs2010 itself installed on the build agent. If that were the case, then the environment would be exactly as it is on a dev machine, complete with the VS100COMNTOOLS environment variable that builds the TeamBuildRefPath MSBuild property, along with the directory that VS100COMNTOOLS references. John, can you verify that the machine you developed this solution on in fact has Vs2010 deployed on it?In the absence of a Vs2010 deployment on the build agent, however, I have found that the following works to fix the immediate problem. I replaced these lines: &lt;!-- Figure out where the TFS build tasks are. --&gt; &lt;TeamBuildRefPath Condition="'$(TeamBuildRefPath)'==''"&gt;$(VS100COMNTOOLS)..\IDE\PrivateAssemblies\&lt;/TeamBuildRefPath&gt;with the following (moved below the PropertyGroup where WintellectBuildType is populated): &lt;PropertyGroup Condition="'$(TeamBuildRefPath)'==''"&gt; &lt;!-- Figure out where the TFS build tasks are. --&gt; &lt;TeamBuildRefPath Condition="'$(WintellectBuildType)'=='DEVELOPERBUILD'"&gt;$(VS100COMNTOOLS)..\IDE\PrivateAssemblies\&lt;/TeamBuildRefPath&gt; &lt;TeamBuildRefPath Condition="'$(WintellectBuildType)'=='TFSBUILD'"&gt;$(ProgramFiles)\Microsoft Team Foundation Server 2010\Tools\&lt;/TeamBuildRefPath&gt; &lt;/PropertyGroup&gt;This is the best I could do, since without an installation of Vs2010 the best environment variable I could use to describe the location of the TeamFoundation.Build binaries was $(ProgramFiles). My build solution is still quite young, so I haven't done much with it yet, but I seem to recall some dependencies on visual studio in the past in cases where you want the build process to do things like perform code analysis and run tests. There are a couple other assemblies located in the same alternative directory, so it's possible that this would work (I'm not sure what the exact requirements are). I'd prefer not to install Vs2010 on each build agent though so as to keep my licensure costs down.It should be noted that TeamBuildRefPath is used to set several other paths in Microsoft.Common.targets, such as test assembly paths, so this might not be a good solution if you're doing anything other than just building. I haven't had time to thoroughly test this yet.But in the meantime, hope it helps!

  17. I'm with Sardar. Struggling to see how to implement. I'm not a MSBuild guru. What do you actually do with these files? What/Where is the "master build file" etc.

  18. This was a great solution to the TFS versioning problem! However there is a small problem in the WriteSharedCSharpAssemblyVersionFile, and the other targets, that generate output the ; (semi-colon) character, it must be MSBuild escaped %3B to be output correctly.

  19. Regarding the calculated year in build number. I am just wondering if I got it right. "=MOD(year-2001,6)" this means on every 7th year I would get the same build number?!
    If so then I would like to change it to "=MOD(year-2000,10)", so I will get the year of current decade. Am I right?

    In consequence this means I have to increment at least major or minor version every ten years to avoid a theoretically chance of having duplicate version numbers? Ok, this scenario to happen has a chance of nearly 0%.

  20. We have assembly info files for every project.
    Each includes useful info .
    How do you use the auto generated csharp content in the existing
    assemblyinfo file?

  21. Your calculation (MOD(year-2001,6)) is only good until 2012. After that you end up with a number larger then 65535.Any suggestions? I would like to not have to modify the calculation.

  22. Anthony,

    Check your math. MOD(2012-2001,6)=5. The maximum for the TFSBuildNumber is 51231.

    Donttellya,

    Are you really going to keep the major version number the same for 10+ years? :) I'm willing to go with six as the divisor as I'm pretty sure I'll do a major release at least once in ten years.

    - John Robbins

  23. Im using the default build template in TFS2010 to build a solution with multiple projects that are delayed-signed. Im getting:
    CSC: Cryptographic failure while signing assembly '...' -- 'Error reading key file 'c:\...Keys.snk' -- The system cannot find the file specified. '
    Question: How to I sign these assemblies in the build process?

  24. Hi,
    First of all thanks for this nice piece of code.
    Is there a way to make TFSMajor/MinorBuildNumber properties visible in visual studio? Tried adding property (.prop) files but without effect.

  25. Just a note for those who are seeing the problem mentioned by Patrik, John and Alex. If you are running VS 2012 instead of VS 2010:
    Change:
    Condition="'$(TeamBuildRefPath)'==''"&gt;$(VS100COMNTOOLS)..\IDE\PrivateAssemblies\
    to:
    Condition="'$(TeamBuildRefPath)'==''"&gt;$(VS110COMNTOOLS)..\IDE\PrivateAssemblies\

  26. For better coverage couldn't you releace the PropertyGroup getting the TeamBuildRefPath to:
    &amp;lt;PropertyGroup&amp;gt;
    &amp;lt;!-- Figure out where the TFS build tasks are. --&amp;gt;
    &amp;lt;TeamBuildRefPath Condition="'$(TeamBuildRefPath)'=='' And '$(VS110COMNTOOLS)'!=''"&amp;gt;$(VS110COMNTOOLS)..\IDE\PrivateAssemblies\&amp;lt;/TeamBuildRefPath&amp;gt;
    &amp;lt;TeamBuildRefPath Condition="'$(TeamBuildRefPath)'=='' And '$(VS100COMNTOOLS)'!=''"&amp;gt;$(VS100COMNTOOLS)..\IDE\PrivateAssemblies\&amp;lt;/TeamBuildRefPath&amp;gt;
    &amp;lt;TeamBuildRefPath Condition="'$(TeamBuildRefPath)'=='' And '$(WintellectBuildType)'=='TFSBUILD'"&amp;gt;$(ProgramFiles)\Microsoft Team Foundation Server 2010\Tools\&amp;lt;/TeamBuildRefPath&amp;gt;
    &amp;lt;!-- Figure out where I'm being called from, TFS Build or a developer
    machine. BuildUri and TeamFoundationServerUrl properties are the
    ones that tell me I'm running under TFS Build.--&amp;gt;
    &amp;lt;WintellectBuildType&amp;gt;DEVELOPERBUILD&amp;lt;/WintellectBuildType&amp;gt;
    &amp;lt;WintellectBuildType Condition="'$(BuildUri)'!='' and '$(TeamFoundationServerUrl)'!=''"&amp;gt;TFSBUILD&amp;lt;/WintellectBuildType&amp;gt;
    &amp;lt;/PropertyGroup&amp;gt;

Leave a Comment

Archives

Tags