55

Original Problem

In building our projects, I want the mercurial id of each repository to be embedded within the product(s) of that repository (the library, application or test application).

I find it makes it so much easier to debug an application being run by customers 8 timezones away if you know precisely what went into building the particular version of the application they are using. As such, every project (application or library) in our systems implements a way of getting at the associated revision information.

I also find it very useful to be able to see if an application has been compiled with clean (un-modified) changesets from the repository. 'Hg id' usefully appends a + to the changeset id when there are uncommitted changes in a repository, so this allows us to easily see if people are running a clean or a modified version of the code.

My current solution is detailed below, and fulfills the basic requirements, but there are a number of problems with it.

Current Solution

At the moment, to each and every Visual Studio solution, I add the following "Pre-build event command line" commands:

cd $(ProjectDir)
HgID

I also add an HgID.bat file to the Project directory:

@echo off
type HgId.pre > HgId.cs

For /F "delims=" %%a in ('hg id') Do <nul >>HgID.cs set /p =            @"%%a"

echo ;                  >> HgId.cs
echo     }              >> HgId.cs
echo }                  >> HgId.cs

along with an HgId.pre file, which is defined as:

namespace My.Namespace {
/// <summary> Auto generated Mercurial ID class. </summary>
internal class HgID {
    /// <summary> Mercurial version ID [+ is modified] [Named branch]</summary>
    public const string Version =

When I build my application, the pre-build event is triggered on all libraries, creating a new HgId.cs file (which is not kept under revision control) and causing the library to be re-compiled with with the new 'hg id' string in 'Version'.

Problems with the current solution

The main problem is that since the HgId.cs is re-created at each pre-build, so every time we need to compile anything, all projects in the current solution are re-compiled. Since we want to be able to easily debug into our libraries, we usually keep many libraries referenced in our main application solution. This can result in build times which are significantly longer than I would like.

Ideally I would like the libraries to compile only if the contents of the HgId.cs file have actually changed, as opposed to having been re-created with exactly the same contents.

The second problem with this method is it's dependence on specific behaviour of the windows shell. I've already had to modify the batch file several times, since the original worked under XP but not Vista, the next version worked under Vista but not XP and finally I managed to make it work with both. Whether it will work with Windows 7 however is anyones guess and as time goes on, I see it more likely that contractors will expect to be able to build our apps on their Windows 7 boxen.

Finally, I have an aesthetic problem with this solution, batch files and bodged together template files feel like the wrong way to do this.

My actual questions

How would you solve/how are you solving the problem I'm trying to solve?

What better options are out there than what I'm currently doing?

Rejected Solutions to these problems

Before I implemented the current solution, I looked at Mercurials Keyword extension, since it seemed like the obvious solution. However the more I looked at it and read peoples opinions, the more that I came to the conclusion that it wasn't the right thing to do.

I also remember the problems that keyword substitution has caused me in projects at previous companies (just the thought of ever having to use Source Safe again fills me with a feeling of dread *8').

Also, I don't particularly want to have to enable Mercurial extensions to get the build to complete. I want the solution to be self contained, so that it isn't easy for the application to be accidentally compiled without the embedded version information just because an extension isn't enabled or the right helper software hasn't been installed.

I also thought of writing this in a better scripting language, one where I would only write HgId.cs file if the content had actually changed, but all of the options I could think of would require my co-workers, contractors and possibly customers to have to install software they might not otherwise want (for example cygwin).

Any other options people can think of would be appreciated.


Update

Partial solution

Having played around with it for a while, I've managed to get the HgId.bat file to only overwrite the HgId.cs file if it changes:

@echo off

type HgId.pre > HgId.cst

For /F "delims=" %%a in ('hg id') Do <nul >>HgId.cst set /p =            @"%%a"

echo ;                  >> HgId.cst
echo     }              >> HgId.cst
echo }                  >> HgId.cst

fc HgId.cs HgId.cst >NUL
if %errorlevel%==0 goto :ok
copy HgId.cst HgId.cs
:ok
del HgId.cst

Problems with this solution

Even though HgId.cs is no longer being re-created every time, Visual Studio still insists on compiling everything every time. I've tried looking for solutions and tried checking "Only build startup projects and dependencies on Run" in Tools|Options|Projects and Solutions|Build and Run but it makes no difference.

The second problem also remains, and now I have no way to test if it will work with Vista, since that contractor is no longer with us.

  • If anyone can test this batch file on a Windows 7 and/or Vista box, I would appreciate hearing how it went.

Finally, my aesthetic problem with this solution, is even stronger than it was before, since the batch file is more complex and this there is now more to go wrong.

If you can think of any better solutions, I would love to hear about them.

Mark Booth
  • 7,605
  • 2
  • 68
  • 92
  • @Mark Booth: first thing that I thought of was your *"better scripting language"* but you're ruling that out explicitely :( Ah the joy of the Microsoft-shop ecosystems and vendor lock-ins ;) – SyntaxT3rr0r Mar 25 '10 at 02:44
  • Sorry, but I know what my colleagues/customers are like and anything which can't be done with a current TortoiseHg+VS2008 install just won't fly. *8'( – Mark Booth Mar 25 '10 at 13:01
  • 2
    In terms of "better scripting" - two things spring to mind: 1) powershell - which a) is a better scripting language and b) is something that it would be much harder to object to than the likes of cygwin; 2) Write a .exe utility (which you can include as controlled in your project) to do the same as your batch file with the additional logic. – Murph Mar 28 '10 at 15:03
  • You may try WSH for better scripting. – Alexey B. Mar 30 '10 at 15:08
  • Powershell is powerful, but is not included by default with XP. It is included by default in 7 (don't know about Vista). – Aram Hăvărneanu Mar 30 '10 at 20:49

7 Answers7

25

I've just released a small open-source MSBuild task to do exactly what you need:

  • It puts your Mercurial revision number into your .NET assembly version
  • You can tell from the version if an assembly has been compiled with uncommitted changes
  • Does not cause unnecessary builds if the revision hasn't changed
  • Not dependent on Windows scripting
  • Nothing to install - you just add a small DLL to your solution, and edit some files in your project

http://versioning.codeplex.com

Joe Daley
  • 45,356
  • 15
  • 65
  • 64
  • 2
    This is very cool. I have started using this in one project and it was a breeze to setup. Thanks Joe. – Mark Stahler Sep 23 '10 at 20:26
  • As already said "very cool". This should be the accepted answer :-) Thanks Joe. – Tim Murphy May 14 '11 at 03:18
  • 5
    I like this idea, but for mercurial I want to use the 160-bit identifier for the changeset, not just the revision number (which is an int) because that isn't consistent across repositories. – Wilka Feb 11 '12 at 19:40
  • @Wilka: true, in Mercurial the revision number is only related to the local clone, so it's not a reliable indication of what went into a certain build. I would like to use the SHA1 as well, but I can't figure out how to do it. Kudos to you anyway, Joe Daley. – s.m. Sep 26 '12 at 14:03
  • This solution will not work for my case either; the local version number is not nearly as useful as the short tag or node id. –  Nov 07 '12 at 19:36
  • 1
    You can use the revision hash! Follow the link on the homepage for the "tokens" you can use. There is a Windows limitation that EXE & DLL version numbers must be integers, so you're stuck with the local revision number for that, but you can embed the revision hash, build date, branch name, tag name, etc into an About dialog for example. Use the same technique to make an AboutDialog.base.cs and AboutDialog.cs. – Joe Daley Nov 07 '12 at 23:00
17

I think I have an answer for you. This will be a bit involved, but it gets you away from having to do any batch files. You can rely on MSBuild and Custom Tasks to do this for you. I've used the extension pack for MSBuild (Available at CodePlex) - but the second task you need is something you could just as easily write yourself.

With this solution, you can right click on the DLL and see in the file properties which Mercurial Version the DLL (or EXE) came from.

Here are the steps:

  1. Get the MBBuildExtension Pack OR Write Custom Task to overwrite AssemblyInfo.cs
  2. Create a Custom Build Task in its own project to get the Mercurial Id(code below).
  3. Edit project files that need the Mercurial Id to use Custom Task (code below).

Custom Task to Get mercurial id: (This would need to be tested well and perhaps better generalized...)

using System;
using System.Diagnostics;
using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;


namespace BuildTasks
{
    public class GetMercurialVersionNumber : Task
    {
        public override bool Execute()
        {
            bool bSuccess = true;
            try
            {
                GetMercurialVersion();
                Log.LogMessage(MessageImportance.High, "Build's Mercurial Id is {0}", MercurialId);
            }
            catch (Exception ex)
            {
                Log.LogMessage(MessageImportance.High, "Could not retrieve or convert Mercurial Id. {0}\n{1}", ex.Message, ex.StackTrace);
                Log.LogErrorFromException(ex);
                bSuccess = false;
            }
            return bSuccess;
        }

        [Output]
        public string MercurialId { get; set; }

        [Required]
        public string DirectoryPath { get; set; }

        private void GetMercurialVersion()
        {
            Process p = new Process();
            p.StartInfo.UseShellExecute = false;
            p.StartInfo.RedirectStandardOutput = true;
            p.StartInfo.RedirectStandardError = true;
            p.StartInfo.CreateNoWindow = true;
            p.StartInfo.WorkingDirectory = DirectoryPath;
            p.StartInfo.FileName = "hg";
            p.StartInfo.Arguments = "id";
            p.Start();

            string output = p.StandardOutput.ReadToEnd().Trim();
            Log.LogMessage(MessageImportance.Normal, "Standard Output: " + output);

            string error = p.StandardError.ReadToEnd().Trim();
            Log.LogMessage(MessageImportance.Normal, "Standard Error: " + error);

            p.WaitForExit();

            Log.LogMessage(MessageImportance.Normal, "Retrieving Mercurial Version Number");
            Log.LogMessage(MessageImportance.Normal, output);

            Log.LogMessage(MessageImportance.Normal, "DirectoryPath is {0}", DirectoryPath);
            MercurialId = output;

        }
    }

And the modified Project File: (The comments may help)

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <!--this is the import tag for the MSBuild Extension pack. See their documentation for installation instructions.-->
  <Import Project="C:\Program Files (x86)\MSBuild\ExtensionPack\MSBuild.ExtensionPack.tasks" />
  <!--Below is the required UsingTask tag that brings in our custom task.-->
  <UsingTask TaskName="BuildTasks.GetMercurialVersionNumber" 
             AssemblyFile="C:\Users\mpld81\Documents\Visual Studio 2008\Projects\LambaCrashCourseProject\BuildTasks\bin\Debug\BuildTasks.dll" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProductVersion>9.0.30729</ProductVersion>
    <SchemaVersion>2.0</SchemaVersion>
    <ProjectGuid>{D4BA6C24-EA27-474A-8444-4869D33C22A9}</ProjectGuid>
    <OutputType>Library</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>LibraryUnderHg</RootNamespace>
    <AssemblyName>LibraryUnderHg</AssemblyName>
    <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Core">
      <RequiredTargetFramework>3.5</RequiredTargetFramework>
    </Reference>
    <Reference Include="System.Xml.Linq">
      <RequiredTargetFramework>3.5</RequiredTargetFramework>
    </Reference>
    <Reference Include="System.Data.DataSetExtensions">
      <RequiredTargetFramework>3.5</RequiredTargetFramework>
    </Reference>
    <Reference Include="System.Data" />
    <Reference Include="System.Xml" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Class1.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

  <Target Name="Build" DependsOnTargets="BeforeBuild">
    <!--This Item group is a list of configuration files to affect with the change. In this case, just this project's.-->
    <ItemGroup>
      <AssemblyInfoFiles Include="$(MSBuildProjectDirectory)\Properties\AssemblyInfo.cs" />
    </ItemGroup>
    <!--Need the extension pack to do this. I've put the Mercurial Id in the Product Name Attribute on the Assembly.-->
    <MSBuild.ExtensionPack.Framework.AssemblyInfo AssemblyInfoFiles="@(AssemblyInfoFiles)"
                                                  AssemblyProduct="Hg: $(MercurialId)"
                                                  />
    <!--This is here as an example of messaging you can use to debug while you are setting yours up.-->
    <Message Text="In Default Target, File Path is: @(AssemblyInfoFiles)" Importance="normal" />
  </Target>  

  <Target Name="BeforeBuild">
    <!--This is the custom build task. The Required Property in the task is set using the property name (DirectoryPath)-->
    <BuildTasks.GetMercurialVersionNumber DirectoryPath="$(MSBuildProjectDirectory)">
      <!--This captures the output by reading the task's MercurialId Property and assigning it to a local
          MSBuild Property called MercurialId - this is reference in the Build Target above.-->
      <Output TaskParameter="MercurialId" PropertyName="MercurialId" />
    </BuildTasks.GetMercurialVersionNumber>
  </Target>
  <!--<Target Name="AfterBuild">
  </Target>-->

</Project>

Last Note: The build tasks project only needs to be built once. Don't try to build it every time you do the rest of your solution. If you do, you will find that VS2008 has the dll locked. Haven't figured that one out yet, but I think the better thing to do is build the dll as you want it, then distribute ONLY the dll with your code, ensuring that the dll's location is fixed relative to every project you need to use it in. That way, no one has to install anything.

Good luck, and I hope this helps!

Audie

Jon Adams
  • 24,464
  • 18
  • 82
  • 120
Audie
  • 1,470
  • 10
  • 12
  • @Audie - Thanks, I'd just found out about the MBBuildExtension Pack from another SO article, so it's good to know it can be made to work. This may not be quite a direct answer, but as with the answer from @dthorpe it points me in a primising direction. – Mark Booth Mar 29 '10 at 17:58
5

Have you considered using a string resource instead of a C# language string constant? String resources can be edited/replaced in the output binaries post-build using tools intended for localization.

You would emit your mercurial version number to a text file that is not used by the C# build, then using a post-build operation replace the version resource with the actual value from the emitted text file. If you strong-name sign your assemblies, the resource string replacement would need to happen before the signing.

This is how we handled this issue at Borland years ago for Windows products. The world has become more complicated since then, but the principle still applies.

dthorpe
  • 35,318
  • 5
  • 75
  • 119
  • @dthorpe - Thanks, that's an intersting idea. Plus, if someone around here knows how to do localisation, we might be able to consider offering a localised version of our software to our main customer in China. *8') – Mark Booth Mar 29 '10 at 18:00
  • @Mark Localization is actually pretty easy. I localized a small app I wrote into Japanese and German, and I'm simply an amateur. Of course, with large, complex, and commercial software, it'll be more of a chore, but the principle is easy. You just get all your UI text from a string resource, and create alternate localized resources for each language. – Benny Jobigan Mar 30 '10 at 09:07
2

Im my .hgrc I have this:

[extensions]                                                                                                                                                               
hgext.keyword =                                                                                                                                                            
hgext.hgk =                                                                                                                                                                

[keyword]                                                                                                                                                                  
** =                                                                                                                                                                       

[keywordmaps]                                                                                                                                                              
Id = {file|basename},v {node|short} {date|utcdate} {author|user} 

And in my source file (Java) I do:

public static final String _rev = "$Id$";

After commit $Id$ gets expanded into something like:

public static final String _rev = "$Id: Communication.java,v 96b741d87d07 2010/01/25 10:25:30 mig $";
kovica
  • 2,443
  • 3
  • 21
  • 25
  • Thanks Kovica, but as I said, I rejected using the keyword extension as it would be too easy to end up with the string "$Id$" being compiled into the executable. If keyword expansion was built into mercurial rather than an extension, and on by default, I might consider it, but as it stands it just wouldn't be reliable. – Mark Booth Mar 17 '10 at 03:57
  • A don't think that there can be a more reliable solution. What if someone accidentally damages .hg or builds not from a `clone` but from an `archive`? – Alexey B. Mar 30 '10 at 15:18
  • 1
    @Mr.Cat - I don't think there can be a **less** reliable solution than the keywords extension. Anywhere you haven't explicitly enabled the extension (or someone has disabled it) then you get the literal string `"$ID$"` compiled into the object file without complaint. If mercurial or the repo is damaged (not sure which you meant) you need to fix that first anyway. As for `hg archive`, my original solution fails to compile if you try to build it from an archive! That is **precisely** what I want. I don't want any source to be compiled into our apps without it source being under revision control! – Mark Booth Mar 30 '10 at 16:32
2

We have solved this issue with another source control system, subversion.

What we do is that we have a commonassemblyinfo.cs file and we plug the svn revision number into that using an msbuild script.

We literally call svninfo and scrape the output from the revision: part and then poke it into the CommonAssemblyInfo.cs file.

Every project in our solution has a link to the common file and is then compiled, meaning that the assemblies are versioned on compilation with the svn revision number, it also means all dependent libraries we have written are also versioned.

We acheived this quite easily using cruisecontrol .net and msbuild files.

I have not used mercurial but I beleive you could do something similar with the id command ? but because of the format of the output you would need to approach how you used it somewhat differently.

I would have a standard xml file that is read on app startup and you could poke the information into that (works for resx files also). You can then read that value back out in code. A bit icky but it works.

My favourite solution is the way we do it but you can't do that in mercurial as the changeset info is not purely numeric like the svn revision number is.

krystan honour
  • 6,523
  • 3
  • 36
  • 63
  • @krystan honour, yup, that's pretty much what the above scripts do (the 'hg id' line is embedded in the For line of HgId.bat). What I'm most interested in is the changeset id of all of the component parts of he project, so that I can log every library compiled into the application. In the future, when we start using subrepos to manage the whole application, we might want to use the changeset id of top level repo to define the actual application version, but that's a way off yet. – Mark Booth Mar 26 '10 at 18:32
1

There seems to be multiple possible approaches to this problem. The first and probably preferred solution would be to install a build server and only distribute builds generated there to customers. This has the advantage that you never ship uncommitted changes. By using MSBuild, NAnt or some other task-based build tool the entire process is very flexible. I was able to install TeamCity and get the first couple of builds up and running with very little effort, but there are other good build servers too. This really should be your solution.

If you for some reason insist that it's okay to distribute developer builds to clients ;) then you'll need a local solution.

A fairly easy solution would be to use the built-in support for auto-incrementing the build number of an assembly:

// major.minor.build.revision
[assembly:AssemblyVersion("1.2.*")]

The * makes the build number auto-increment every time you compile (and there are changes). The revision number is a random number. From here you can either keep track of the association to the Mercurial id by saving both pieces of information, e.g. by posting it to some internal web solution or whatever fits your particular needs, or update the generated assembly. I'd suspect you could use PostSharp or Mono.Cecil do rewrite the assembly, e.g. by patching the revision number to be the id. If your assemblies are signed the rewrite needs to happen before you sign them, which is a bit bothersome if you don't have a build file. Note that VS can be configured to compile using your custom build file instead of the default build procedure.

My final suggestion is to create a separate project just for the hg id, and use the post-build step to merge the generated assemblies into one. ILMerge supports re-signing of signed assemblies and this is therefore likely to be easier to make work. The downside is that redistribution of ILMerge is not allowed (although commercial use is).

It's not a solution but hopefully inspiration to get you going.

Morten Mertner
  • 9,414
  • 4
  • 39
  • 56
  • 1
    @Morten Mertner, The problem with AssemblyVersion is that it tells you nothing about the source code. It gets even more complex when you have a branchy development model. If someone changes one particular library from the (stable) branch to the (experimental) branch, I want to be able to see that change in my log file. Using the 'hg id' tells me exactly what code was compiled into the library, and whether it has been modified. I can often then go back to the DVCS, update to that version, recompile and (hopefully) reproduce any problems my customer might be having. – Mark Booth Mar 26 '10 at 18:33
1

Here's what we do here: we do not embed the revision every time the project is built on developer's machines. At best this causes the problems you've mentioned above, and at worst it's misleading because you could have modified files in your workspace.

Instead we only embed the source control revision number when our projects are built under TeamCity on a separate build server. The revision is embedded into both AssemblyVersion and AssemblyFileVersion, split across the last two parts.

By default the version ends in 9999.9999, and we split the revision in such a way that revision 12345678 would become 1234.5678 (not that we're anywhere close to the 12 millionth revision...).

This process guarantees that a product whose version is 2.3.1.1256 was definitely a pristine build of revision 11,256. Anything developers build manually will instead look like this: 2.3.9999.9999.

The exact details of how the above is achieved are not directly relevant since we're not using Mercurial, but briefly: TeamCity handles checking out the required revision and passes its number to our MSBuild script, which then does the rewriting of AssemblyInfo.cs with the help of a custom task we wrote.

What I particularly like about this is that absolutely no files are modified by hitting F5 in Visual Studio - in fact, as far as Visual Studio is concerned, it's working with a plain old normal solution.

Roman Starkov
  • 59,298
  • 38
  • 251
  • 324
  • 1
    @romkyns, thanks but this is precisely the situation I'm trying to avoid. I want to know which mercurial chagesets were used to build any given instance of the application. We often leave source code on customer machines and sometimes field service engineers make small modifications. By embedding the Hg id, I can tell if they are running a committed version and if not, which library was changed. This in itself is useful, even if I can't tell what changes they've made until they commit them and send us an update bundle. – Mark Booth Mar 29 '10 at 10:58