Category: Version Control

  • Integrating the Mercurial Revision into the Version Automatically with .NET

    I’ve already shown how you can add the mercurial revision into the version automatically with native C/C++. However, there are some extra hurdles you have to jump to make this work for .NET. The problem is that you can’t use static variables or class data in the assembly attributes – you have to use constant literals or expressions. As a result, we can’t just generate a simple class and reference it in the AssemblyAttributes.

    Again I’ll be building on the previous posts and discussing how to do this with Visual Studio 2010 Professional and Mercurial using TortoiseHg, this time for a .NET application. Other versions of Visual Studio should work similarly and other Mercurial packages will work as well, as long as they provide command-line tools that are in the path of your IDE. We’ll be using a GlobalAssemblyInfo file to share common assembly attributes across projects in the same solution as discussed here.

    The source code for this post is available here.

    Step 1: Add a Version Project

    Although I didn’t do this for the native solution, I find adding a specific Version project to the solution is beneficial. The reason is that we are generating common version info to be used across the entire solution, however the first project in the build order needs to be the one to generate this info. Depending on your project dependencies there may not be an obvious or good place to do this. By adding a Version project and making all other projects depend on it we have an easy place to generate the necessary files.

    1. Add a project named Version to your Solution
    2. Add a reference to Version from all the other projects in your Solution

    Step 2: Add a GlobalAssemblyInfo Template

    Rather than generate the entire contents of the GlobalAssemblyInfo.cs file in the script, where it’s harder to find and edit when needed, we’ll use a template. Create a file named GlobalAssemblyInfo.cs.tmpl in the Properties folder of the Version project. Copy the code below into the template and make any desired customizations to match your environment.

    Notice the $REVISION$, $CHANGESET$ and $LOCAL_MODIFICATIONS$ tokens. We’ll use these to place the necessary Mercurial information in the file we generate.

    // This file contains common AssemblyVersion data to be shared across all projects in this solution.
    using System.Reflection;
    
    [assembly: AssemblyCompany("Zach Burlingame")]
    [assembly: AssemblyProduct("DotNetHgAutoVersion")]
    [assembly: AssemblyCopyright("Copyright © Zach Burlingame 2011")]
    [assembly: AssemblyTrademark("")]
    [assembly: AssemblyCulture("")]
    
    #if DEBUG
    [assembly: AssemblyConfiguration("Debug")]
    #else
    [assembly: AssemblyConfiguration("Release")]
    #endif
    
    // Version information for an assembly consists of the following four values:
    //
    //      Major Version
    //      Minor Version
    //      Revision
    //      Build
    [assembly: AssemblyVersion("1.0.0.$REVISION$")]
    [assembly: AssemblyTitle("$CHANGESET$$LOCAL_MODIFICATIONS$")]   // This is visible as the "File Description" on the Details tab of the Explorer pane
    

    Step 3: Using WSH to Generate Global Assembly

    Next we need to add a file to our Version project named hg_version.jse. Personally, I like to add this file under the Properties filter of my project. Copy the code below in to the script file. This code does two things:

    1. Creates GlobalAssemblyInfo.cs
    2. Extracts the desired Mercurial version info from the working copy and places it in the header

    The extracted version info includes the full node identity, the revision number and if there are any local modifications to the working copy. Note that in counter-intuitive fashion, the AssemblyTitle attribute is what sets the Description field when viewed from the Details tab of the Properties pane in Explorer. Meanwhile the AssemblyDescription field isn’t displayed at all and rather is only accessible through API calls against the binary. Why MS did this, I do not know…

    var fso   = new ActiveXObject("Scripting.FileSystemObject");
    var shell = new ActiveXObject("WScript.Shell");
    var ForReading = 1, ForWriting = 2, ForAppending = 8;
    
    var projectDir = "../../";
    
    var hgRevNum               = shell.Exec("hg identify --num");
    var rev                    = hgRevNum.StdOut.ReadAll();
    var hg_revision            = String(rev).replace(/\n/g,"").replace(/\+/g,"");
    var hg_local_modifications = '';
    
    if( String(rev).replace(/\n/g, "").indexOf("+") != -1 )
    {
       hg_local_modifications = '+';
    }
    
    var hgChangeset  = shell.Exec("hg parents --template \"{node}\"");
    var changeset    = hgChangeset.StdOut.ReadAll();
    var hg_changeset = String(changeset).replace(/\n/g,"");
    
    var hgChangesetShort    = shell.Exec("hg parents --template \"{node|short}\"");
    var changeset_short     = hgChangesetShort.StdOut.ReadAll();
    var hg_changeset_short  = String(changeset_short).replace(/\n/g,"");
    
    var tmplFile = fso.OpenTextFile( projectDir + 'Properties/GlobalAssemblyInfo.cs.tmpl', ForReading, false );
    var strContents = tmplFile.ReadAll();
    tmplFile.Close();
    
    strContents = String(strContents).replace(/\$REVISION\$/g, hg_revision );
    strContents = String(strContents).replace(/\$LOCAL_MODIFICATIONS\$/g, hg_local_modifications );
    strContents = String(strContents).replace(/\$CHANGESET\$/g, hg_changeset );
    strContents = String(strContents).replace(/\$SHORT_CHANGESET\$/g, hg_changeset_short )
    
    var asmFile = fso.CreateTextFile( projectDir + '../GlobalAssemblyInfo.cs', ForWriting, true );
    asmFile.Write( strContents );
    asmFile.Close();
    

    Step 4: Add a Pre-build Event

    Add a pre-build event to the Version project to call the hg_version.jse script and generate GlobalAssemblyInfo.cs.

    1. Right-click on the Version project
    2. Select Properties
    3. Select the Build Events tab
    4. In the Pre-build event command line box, enter:
      cscript.exe “$(ProjectDir)\Properties\hg_version.jse”

    Step 5: Add a link to the GlobalAssemblyInfo.cs file in each project

    1. In Visual Studio, right-click on a project
    2. Select Add->Existing Item
    3. Browse to GlobalAssemblyInfo.cs
    4. Select the file
    5. Click the drop-down arrow next to Add and select Add As Link
    6. Move the link to your Properties folder (optional, but keeps things neat)

    Step 6: Update the AssemblyInfo.cs for each project

    In order to avoid duplicate annotations for assembly information, you must remove entries from the AssemblyInfo.cs file that appear in the GlobalAssemblyInfo.cs file. In our example here, this is what we end up with:

    using System.Reflection;
    using System.Runtime.CompilerServices;
    using System.Runtime.InteropServices;
    
    // General Information about an assembly is controlled through the following
    // set of attributes. Change these attribute values to modify the information
    // associated with an assembly.
    [assembly: AssemblyDescription("")]  // This is not visible on the Details tab of the Explorer pane
    
    // Setting ComVisible to false makes the types in this assembly not visible
    // to COM components.  If you need to access a type in this assembly from
    // COM, set the ComVisible attribute to true on that type.
    [assembly: ComVisible(false)]
    
    // The following GUID is for the ID of the typelib if this project is exposed to COM
    [assembly: Guid("dad09178-814d-43f4-b76d-0fbe29a32544")]
    

    Conclusion

    And there you have it! Now when you build your solution, all your project assemblies will have the latest Mercurial version information included in their meta-data automatically.

    Other Posts in this Series

    1. Mapping Binaries in the Field to Source Code in the Repository
    2. Versioning a Native C/C++ Binary with Visual Studio
    3. Versioning a .NET Assembly with Visual Studio
    4. Integrating the Mercurial Revision into the Version Automatically with Native C/C++
    5. Integrating the Mercurial Revision into the Version Automatically with .NET
    6. Integrating the Subversion Revision into the Version Automatically with Native C/C++
    7. Integrating the Subversion Revision into the Version Automatically with .NET
  • Integrating the Mercurial Revision into the Version Automatically with Native C/C++

    Now that we’ve covered why we should include version information in our binaries and how to do that for native C/C++ and managed .NET projects, it’s time to up our game. One of the major shortfalls of the previous solutions is that the version information on the binary didn’t map one-to-one to revisions from source control. As a result, while you may be able to determine the version of a particular file, you can’t easily get to the corresponding source code, if at all. One solution to this is to include the source control revision information in the version so you know exactly what was used to build it. Here I’ll be building on the previous posts and discussing how to do this with Visual Studio 2010 Professional and Mercurial using TortoiseHg for a native C/C++ application. Other versions of Visual Studio should work similarly and other Mercurial packages will work as well, as long as they provide command-line tools that are in the path of your IDE. The steps can be easily modified for use with a .NET project based on the previous post.

    Update 2011/02/17: Doing this for a .NET assembly is actually a bit trickier than I originally thought because you can’t use static variables or class data in the assembly attributes – you have to use constant literals or expressions. I’ll do a separate post on how to do this with a .NET project.

    The basic concept of how this works is explained in the here. The source code for this post is available here.

    Step 1: Using WSH to Generate a Version Header

    The first step is to add a file to your project named hg_version.jse. Personally, I add this file under the Resource filter of my project. Copy the code below in to the script file. This code does two things:

    1. Creates hg_version.h
    2. Extracts the desired Mercurial version info from the working copy and places it in the header

    The extracted version info includes the full node identity, the short node identity, the revision number, and if there are any local modifications to the working copy.

    var fso   = new ActiveXObject("Scripting.FileSystemObject");
    var shell = new ActiveXObject("WScript.Shell");
    
    var outFile = fso.CreateTextFile("hg_version.h", true);
    
    var hgRevNum               = shell.Exec("hg identify --num");
    var rev                    = hgRevNum.StdOut.ReadAll();
    var hg_revision            = String(rev).replace(/\n/g,"").replace(/\+/g,"");
    var hg_local_modifications = 0
    if( String(rev).replace(/\n/g, "").indexOf("+") != -1 )
    {
       hg_local_modifications = 1;
    }
    outFile.WriteLine( "#define HG_REVISION               " + hg_revision );
    outFile.WriteLine( "#define HG_LOCAL_MODIFICATIONS    " + hg_local_modifications );
    
    var hgChangeset  = shell.Exec("hg parents --template \"{node}\"");
    var changeset    = hgChangeset.StdOut.ReadAll();
    var hg_changeset = String(changeset).replace(/\n/g,"");
    outFile.WriteLine( "#define HG_CHANGESET              \"" + hg_changeset +"\"" );
    
    var hgChangesetShort    = shell.Exec("hg parents --template \"{node|short}\"");
    var changeset_short     = hgChangesetShort.StdOut.ReadAll();
    var hg_changeset_short  = "#define HG_CHANGESET_SHORT        \"" + String(changeset_short).replace(/\n/g,"") + "\"";
    outFile.WriteLine( hg_changeset_short );
    
    outFile.Close();
    

    Step 2: Update Version.h

    The version.h file we created in a previous post needs to be updated to use the information from the generated header. The mercurial revision number is a human friendly integer and can be used directly in the file version. However, due to the nature of a DVCS it is not guaranteed to be globally unique (and it often won’t be on projects with multiple developers using common workflow patterns). The node identity however, does uniquely identify the changeset globally. We include this information in the file description field which maps the binaries one-to-one with the source code they were built with. It’s important to note that the identity field is not a 16-bit integer and thus cannot be used in the file version field directly. Finally, we want to know if the binary was built with local modifications to the working copy, which would complicate reproducing the build. As a result we append an ‘M’ to the end of the file version string if local modifications are present.

    #include "hg_version.h"
    
    #define STRINGIZE2(s) #s
    #define STRINGIZE(s) STRINGIZE2(s)
    
    #define VERSION_MAJOR               1
    #define VERSION_MINOR               0
    #define VERSION_REVISION            0
    #define VERSION_BUILD               HG_REVISION
    
    #if HG_LOCAL_MODIFICATIONS
      #define VERSION_MODIFIER "M"
    #else
      #define VERSION_MODIFIER
    #endif
    
    #define VER_FILE_DESCRIPTION_STR    HG_CHANGESET
    #define VER_FILE_VERSION            VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION, VERSION_BUILD
    #define VER_FILE_VERSION_STR        STRINGIZE(VERSION_MAJOR)        \
                                        "." STRINGIZE(VERSION_MINOR)    \
                                        "." STRINGIZE(VERSION_REVISION) \
                                        "." STRINGIZE(VERSION_BUILD)    \
                                        VERSION_MODIFIER
    
    #define VER_PRODUCTNAME_STR         "c_hg_autoversion"
    #define VER_PRODUCT_VERSION         VER_FILE_VERSION
    #define VER_PRODUCT_VERSION_STR     VER_FILE_VERSION_STR
    
    #if LIBRARY_EXPORTS
      #define VER_ORIGINAL_FILENAME_STR VER_PRODUCTNAME_STR ".dll"
    #else
      #define VER_ORIGINAL_FILENAME_STR VER_PRODUCTNAME_STR ".exe"
    #endif
    #define VER_INTERNAL_NAME_STR       VER_ORIGINAL_FILENAME_STR
    
    #define VER_COPYRIGHT_STR           "Copyright (C) 2011"
    
    #ifdef _DEBUG
      #define VER_VER_DEBUG             VS_FF_DEBUG
    #else
      #define VER_VER_DEBUG             0
    #endif
    
    #define VER_FILEOS                  VOS_NT_WINDOWS32
    #define VER_FILEFLAGS               VER_VER_DEBUG
    
    #if LIBRARY_EXPORTS
      #define VER_FILETYPE              VFT_DLL
    #else
      #define VER_FILETYPE              VFT_APP
    #endif
    

    Step 3: Add the Pre-build Step

    Finally we need to add a pre-build step which will execute the hg_version.jse script, thus generating the hg_version.h file prior to the binary being built.

    1. Right-click on your project
    2. Select Properties
    3. Click Build Events
    4. Click Pre-Build Event
    5. In the Configuration drop-down, select All Configurations
    6. In the Command Line field, enter:
      cscript.exe hg_version.jse
    7. In the Description field, add a comment such as:
      Generate the hg_version.h file with the necessary repo identify info for versioning

    NOTE:If you have multiple projects in the same solution that all need to use the same information from Mercurial you have a few choices. One is to put the hg_version.jse script in one project which all the other projects depend on and add a link to the hg_version.h file. Another option is to create a specific version project that all it does is generate the hg_version.h file and define common version information and then have every project in the solution depends on it so it’s executed first in the build order.

    Results

    Now when each time you build your projects, the latest Mercurial information of the working copy is automatically included in the file version information.

    Version Info with Mercurial
    Version Info with Mercurial

    Final Thoughts

    So there you have it, you can now automatically include all the necessary information from your Mercurial working copy in your build to map them one-to-one with the source code that was used. In an upcoming post I’ll discuss how to do this using Subversion as your VCS.

    Other Posts in this Series

    1. Mapping Binaries in the Field to Source Code in the Repository
    2. Versioning a Native C/C++ Binary with Visual Studio
    3. Versioning a .NET Assembly with Visual Studio
    4. Integrating the Mercurial Revision into the Version Automatically with Native C/C++
    5. Integrating the Mercurial Revision into the Version Automatically with .NET
    6. Integrating the Subversion Revision into the Version Automatically with Native C/C++
    7. Integrating the Subversion Revision into the Version Automatically with .NET
  • Mapping Binaries in the Field to Source Code in the Repository

    The Scenario

    Andrew from customer support walks down to your cubicle (wait, you have an office?! jealous.) and tells you that he has Joanne on the line from Widgets and Wrenches Unlimited. She says that your remote login software is crashing and they can’t access the server that controls the CNC machine which means they can’t make tools. One of the first things you are going to want to know, is “What version of the app are you running?”

    Usually if it’s a product or mature application, there will be a straightforward way to get this information – an About page or the version name right in the application shortcut for instance. When it’s a smaller app or library, or when you need more specific information than “v1.2”  though, you might be up a creek. Numerous times I’ve run into this problem on an internal application where there is no installer, about page, or any other information on the version of the binary I’m looking at and what it’s origins might be.  Sometimes it’s as simple as “let’s just grab the latest version and try that”, but other times that’s not an option. My solution to this problem on many projects has been to include the version information in the binary or assembly itself. At the very least someone can view the Details tab on the Properties window in Explorer and get the info directly.  Best of all it works on all versions of Windows, even if the application won’t run.

    A Solution

    There are tons of different versioning schemes out there, but I tend to favor:

    MAJOR.MINOR.REVISION.BUILD

    Three things have been key for making this work for me:

    1. Make the least significant number (BUILD in my example above) correspond to the revision number from the source control repository.
    2. Note if the binary was built from a working copy with local modifications
    3. Make it happen automatically.

    With these three things, we can take a binary and know exactly what revision we need to grab from source control to reproduce the problem. Before we release a binary to anyone, we can make sure it wasn’t built with local modifications (and thus potentially seriously complicating reproducing the problem). And since it all happens automatically, we don’t have to remember jump through any hoops. Yay!

    How-To

    The basic concept is that a pre-build step runs which grabs the version information from the source control tool for the working copy and generates an file which is included in our project. From there we include the information into our version scheme and then it’s just applied to a normal resource version file or AssemblyInfo.cs.

    Another policy I try follow on my projects is having everything build on checkout with as minimal a development environment as possible (usually just Visual Studio and the source control tools). To that end, I try to avoid bringing a scripting engine like Python into the development environment just to allow a simple build script. So to accomplish that pre-build step, I use the Javascript engine for the Windows Script Host (WSH) which is built-in since Windows 98.

    Over the next several posts, I’m going to explain how to accomplish this in Visual Studio. I’ll explain how to do it for a native C/C++ binary and a C# assembly as well as automating the BUILD number from Subversion and Mercurial.

    Other Posts in this Series

    1. Mapping Binaries in the Field to Source Code in the Repository
    2. Versioning a Native C/C++ Binary with Visual Studio
    3. Versioning a .NET Assembly with Visual Studio
    4. Integrating the Mercurial Revision into the Version Automatically with Native C/C++
    5. Integrating the Mercurial Revision into the Version Automatically with .NET
    6. Integrating the Subversion Revision into the Version Automatically with Native C/C++
    7. Integrating the Subversion Revision into the Version Automatically with .NET