John Robbins' Blog

TFS 2010 Build Numbers & File Versions from Inside Your C# and C++ Projects

A while ago, I showed using MSBuild 4.0 to create build version files with the TFS build number so that build number could be included in your binaries. While you can use Jim Lamb's excellent custom workflow activity, I liked doing the version numbers with MSBuild because you could have the same build for both the build server as well as the developer desktop.

Working with a client, they like my approach, but didn't care for the fact you had to initially create the assembly version files with a command line build. Additionally, there was good security and process on TFS build servers so adding the custom workflow activity was going to take a while to get installed. They asked me to get my pure MSBuild approach working inside Visual Studio projects so they could get the benefit of the build numbers, but not change how anyone builds the code on their development box. The client was kind enough to let me blog this so everyone could benefit. That and they wanted documentation on how I got it working. <grin!>

For the rest of this article, I'm assuming you've read my original blog entry on Wintellect.TFSBuildNumber.targets. I've put everything I discuss into a sample project that shows how to set this up for both C#/VB projects as well as for native C++ projects. You may want to grab the sample and look through it as you read along.

The job of Wintellect.TFSBuildNumbers.targets is to create files with build number information into them. You'll include those created files into your projects in order to get the version information, be it an AssemblyFileVersion attribute or a VERSIONINFO structure. Because the files that contain the version information should never be checked in, you need to create those files first thing or your build will fail. That means you'll need to do the steps I outline below in the first .CSPROJ/.VCSPROJ file built as part of your solution. Ideally, you'll only have to update a single project that every other binary depends on as described below to create the version files for your whole application. However, if you have various solutions and you don't know the order they will be built, it won't hurt to add these steps to all your projects as Wintellect.TFSBuildNumbers.targets only creates the build version files if they do not exist.

If you've got a C# .CSPROJ file as the first thing built, it's quite easy to integrate the version file creation code. To edit the .CSPROJ file by hand, click the File, Open, File, menu in Visual Studio.

In the PropertyGroup element that has no conditions on it, generally the first one after the Project element. You'll add the TFSMajorBuildNumber and TFSMinorBuildNumber elements with the appropriate values. By default Wintellect.TFSBuildNumbers.targets creates the shared files in the local directory so you should probably also set the SharedVersionOutputDirectory element to a common location. The following shows the first part of the .\BuildNumbers\VerFiles\VerFiles.CSPROJ file included with the sample.

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build"
   xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">x86</Platform>
    <ProductVersion>8.0.30703</ProductVersion>
    <SchemaVersion>2.0</SchemaVersion>
    <ProjectGuid>{826C2BC7-70D3-4FE0-B4D8-0E1DE36E6D2E}</ProjectGuid>
    <OutputType>Exe</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>VerFiles</RootNamespace>
    <AssemblyName>VerFiles</AssemblyName>
    <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
    <TargetFrameworkProfile>Client</TargetFrameworkProfile>
    <FileAlignment>512</FileAlignment>
    <!-- The following two properties are mandatory with
         Wintellect.TFSBuildNumber.targets-->
    <TFSMajorBuildNumber>22</TFSMajorBuildNumber>

    <TFSMinorBuildNumber>33</TFSMinorBuildNumber>
    <!-- The following property is optional with

         Wintellect.TFSBuildNumber.targets but you probably
         should always set it.-->

    <SharedVersionOutputDirectory>..\Shared</SharedVersionOutputDirectory>
  </PropertyGroup>
. . . Rest of file here . . .

With the properties out of the way, you'll scroll to the bottom of the .CSPROJ file where you will import the Wintellect.TFSBuildNumber.targets file with an Import element. C# projects have a nice target, BeforeBuild which is a target the build system will call before doing the actual build. You will need to add that target at the bottom of the file and use the DependsOnTargets to specify the various files you want to Wintellect.TFSBuildNumbers.targets to create.

Here the example of setting up the import and BeforeBuild target.

  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
  <!-- Include the targets that create the version number files. -->
  <Import Project="..\Targets\Wintellect.TFSBuildNumber.targets" />
  <!-- Define the BeforeBuild target, which is normally commented out,
       to depend on the particular version files I need created. -->
  <Target Name="BeforeBuild"
                DependsOnTargets="WriteSharedTextAssemblyVersionFile;
                                  WriteSharedCSharpAssemblyVersionFile;
                                  WriteSharedCPPAssemblyVersionFile;
                                  WriteSharedWiXAssemblyVersionFile;">
  </Target>
  <!-- To modify your build process, add your task inside one of the targets below
       and uncomment it.

       Other similar extension points exist, see Microsoft.Common.targets.
  <Target Name="AfterBuild">
  </Target>
  -->
</Project>

If you open the .\BuildNumbers\VerFiles\VerFiles.SLN and build it, you'll see four new files in the .\BuildNumbers\Shared directory. You'll also notice the project has a reference to the.\Shared\SharedAssemblyFileVersion.cs that was as a linked file. As a final reminder, because you'll be including the AssemblyFileVersion from Wintellect.TFSBuildNumber.targets, you'll need to manually remove any existing AssemblyFileVersion attributes in your projects.

Incorporating Wintellect.TFSBuildNumber.targets into a C++ project is similar to what you'll do for a C# project, but there's a little more fiddling going on. C++ is supposed to be harder than C#, so that's normal for the course. You can see a full example of setting up a C++ project in the .\BuildNumbers\CPPVerFiles directory in the sample download. The first step is to edit the .VCXPROJ file by hand to add the required properties with your major and minor versions. Look for the PropertyGroup element with the Label attribute set to Globals. Here's an example of setting up the properties.

  <PropertyGroup Label="Globals">
    <ProjectGuid>{B1CCCB99-E2C5-42BA-9680-8E9511CE81C7}</ProjectGuid>
    <RootNamespace>CPPVerFiles</RootNamespace>
    <Keyword>MFCProj</Keyword>

    <!-- The following two properties are needed by
         Wintellect.TFSBuildNumber.targets-->
    <TFSMajorBuildNumber>22</TFSMajorBuildNumber>
    <TFSMinorBuildNumber>33</TFSMinorBuildNumber>
    <!-- The following property writes the shared files to
         A different directory.—>
  <SharedVersionOutputDirectory>..\Shared</SharedVersionOutputDirectory>

</PropertyGroup>

At the bottom of your .VCXPROJ file, you'll need to import Wintellect.TFSBuildNumber.targets and hook the build number file creation into the build. As .VCXPROJ files do not have a predefined BeforeBuild target, it took me a little bit of trial and error to find where the best place to get my build number targets injected into the normal build flow. Fortunately, MSBuild 4.0 offers the BeforeTargets attribute on a target. As the build number files need to be created before the resource compiler runs, I chose to set the BeforeTargets to ResourceCompile. Here's the snippet that shows you what I'm talking about.

  <!-- Include the targets that create the version number files. -->
  <Import Project="..\Targets\Wintellect.TFSBuildNumber.targets" />

  <!-- Here's the trick to ensure the version files get built before
       they are needed by the resource compiler. -->
  <Target Name="BuildVersionFiles"
          BeforeTargets="ResourceCompile"
          DependsOnTargets="WriteSharedTextAssemblyVersionFile;
                            WriteSharedCSharpAssemblyVersionFile;
                            WriteSharedCPPAssemblyVersionFile;
                            WriteSharedWiXAssemblyVersionFile;">
  </Target>

</Project>

The above steps in the .VCXPROJ file take care of properly creating the files, but we need to get the version number into the binary. Because C++ code needs a VERSIONINFO structure in the resources file, you'll have to include a common one to pick up the build numbers. What I chose to do was have a common .RC file that's included in each of your project's .RC. Below is the .\CommonResources\CommonResources.RC you'll find in the sample.

#ifndef APSTUDIO_INVOKED

#include "..\Shared\SharedAssemblyFileVersion.h"

VS_VERSION_INFO VERSIONINFO
  FILEVERSION rcMajor,rcMinor,rcBuild,rcRevision
  PRODUCTVERSION rcMajor,rcMinor,rcBuild,rcRevision
  FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
  FILEFLAGS 0x1L
#else
  FILEFLAGS 0x0L
#endif
  FILEOS 0x4L
  FILETYPE 0x2L
  FILESUBTYPE 0x0L
BEGIN
  BLOCK "StringFileInfo"
  BEGIN
    BLOCK "040904e4"
    BEGIN
      VALUE "Comment", "De Oppresso Liber"
      VALUE "CompanyName", "Wintellect"
      VALUE "FileDescription", "Example of the common version resource file!"
      VALUE "FileVersion", szMajorMinorBuildRevision
      VALUE "InternalName", "SOMENAME"
      VALUE "LegalCopyright", "Copyright (c) 2011 by Wintellect"
      VALUE "ProductVersion", szMajorMinorBuildRevision
    END
  END
  BLOCK "VarFileInfo"
  BEGIN
    VALUE "Translation", 0x409, 1252
  END
END

#endif

To get this file into your project's .RC file, you'll edit it by hand in simply use #include to include the common file.

There are two key points I need to mention about getting a common resource file working. The first drove me nuts, but you want to include an actual resource, the including file must end with .RC. If you put it in a .H, the resource compiler silently ignores the contents of the file except for #define values. The second point was that I wanted to ensure that there was no way to edit the VERSIONFINO block in the Visual Studio Resource Editor. If that's allowed, the Resource Editor replaces all defines in the resource file with the current values of the build numbers. To avoid that problem, I wrap the common VERSIONINFO in #ifndef APPSTUDIO_INVOKE…#endif. The APPSTUDIO_INVOKE is the special define used to determine if the file is being edited by the Resource Editor. The old folks reading this will recall that the Resource Editor used to be called App Studio many years ago.

Some of you sharp readers might be wondering how you add non-shared data to the VERSIONINFO such as "OriginalFilename" and others as you can only have one VERSIONINFO structure per binary. If that private data is a required for your environment, add a new VALUE for it and before including the common resources, ensure you declare that define. Alternatively, you could use a VERSIONINFO editing tool like the Simple Version Resource Tool for Windows by ddbug as part of your build to add that information.

Based on the comments to my original article, this was a popular request, but I just never got around to doing it because I'm a command line guy and it wasn't a priority. Thanks to our client for pushing me to get build number creation working in projects and allowing me to share the result with everyone. Now there's no excuse for anyone to not have the build number show up when you look at a binary's properties.

On Sep 5 2011 4:14 PMBy jrobbins MSBuild, TFSWith 8 Comments

Comments (8)

  1. John: I have a WPF c# project that I am now using this version of the TFSBuildNumber.targets file. It is working great on my local, but once I checkin and do a build in TFS I an getting an error: error MSB3491: Could not write lines to file. Our Administrator has made sure that the TFS user has access to write to the folder. Interesting fact: the file is there and gets updated...but we still see the error. Any help would be greatly appreciated. ~Doug

  2. John,
    Ran into another issue. We have upgraded from VS2010 to VS2012. both on our Dev machines and the build box. On the next check in which caused a build on the build box we got the following error:
    C:\Builds\1\iNSIGHTImageReview\Image Review\Sources\Image Review\ImageReview\TFSBuildNumber.targets(93,9): error MSB4062: The "Microsoft.TeamFoundation.Build.Tasks.GetBuildProperties" task could not be loaded from the assembly C:\Builds\1\iNSIGHTImageReview\Image Review\Sources\Image Review\ImageReview\..\IDE\PrivateAssemblies\\Microsoft.TeamFoundation.Build.ProcessComponents.dll. Could not load file or assembly 'file:///C:\Builds\1\iNSIGHTImageReview\Image Review\Sources\Image Review\IDE\PrivateAssemblies\Microsoft.TeamFoundation.Build.ProcessComponents.dll' or one of its dependencies. The system cannot find the file specified. Confirm that the (UsingTask) declaration is correct, that the assembly and all its dependencies are available, and that the task contains a public class that implements Microsoft.Build.Framework.ITask. [C:\Builds\1\iNSIGHTImageReview\Image Review\Sources\Image Review\ImageReview\ImageReview.csproj]
    In looking at the TFSBuildNumber.targets file it is setting the location to Microsoft.TeamFoundation.Build.ProcessComponents.dll to be at TeamBuildRefPath which in looking at the log file is pointing to "..\IDE\PrivateAssemblies\".
    I hard coded it to be: C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\IDE\PrivateAssemblies and that worked.
    Is there a way to get to point to the correct location? or should I setup a different path variable and change from TeamBuildRefPath to the new one?
    Thanks ~Doug

  3. John,
    Ran into another issue. We have upgraded from VS2010 to VS2012. both on our Dev machines and the build box. On the next check in which caused a build on the build box we got the following error:
    C:\Builds\1\iNSIGHTImageReview\Image Review\Sources\Image Review\ImageReview\TFSBuildNumber.targets(93,9): error MSB4062: The "Microsoft.TeamFoundation.Build.Tasks.GetBuildProperties" task could not be loaded from the assembly C:\Builds\1\iNSIGHTImageReview\Image Review\Sources\Image Review\ImageReview\..\IDE\PrivateAssemblies\\Microsoft.TeamFoundation.Build.ProcessComponents.dll. Could not load file or assembly 'file:///C:\Builds\1\iNSIGHTImageReview\Image Review\Sources\Image Review\IDE\PrivateAssemblies\Microsoft.TeamFoundation.Build.ProcessComponents.dll' or one of its dependencies. The system cannot find the file specified. Confirm that the (UsingTask) declaration is correct, that the assembly and all its dependencies are available, and that the task contains a public class that implements Microsoft.Build.Framework.ITask. [C:\Builds\1\iNSIGHTImageReview\Image Review\Sources\Image Review\ImageReview\ImageReview.csproj]
    In looking at the TFSBuildNumber.targets file it is setting the location to Microsoft.TeamFoundation.Build.ProcessComponents.dll to be at TeamBuildRefPath which in looking at the log file is pointing to "..\IDE\PrivateAssemblies\".
    I hard coded it to be: C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\IDE\PrivateAssemblies and that worked.
    Is there a way to get to point to the correct location? or should I setup a different path variable and change from TeamBuildRefPath to the new one?
    Thanks ~Doug

  4. Doug,

    That's not too bad. However, I updated the code to handle TFS 2012 and forgot to post it. I'll try to get that posted when I get off this plane about to land. (Isn't modern technology great!?!?)

    - John Robbins

  5. John,
    Thanks for posting the new version fro VS2012. Had to make a couple tweaks to it. Apparently out build box is already formatting the BuildNumber with the correct information along with the project name as a prefix. So I just wound up using that.
    We also ran into the issue which I posed earlier where we could not write to the CSharpAssemblyVersionFile. We added the following line just before attempting to write the file.
    (Exec Condition="Exists('$(CSharpAssemblyVersionFile)')" Command='attrib -R "$(CSharpAssemblyVersionFile)"' IgnoreExitCode='true' /) .. Change the start and end () to LessThan GreaterThan symbols
    And then everything was good.
    Thanks so very much for your help.
    ~Doug

Leave a Comment

Archives

Tags