Register | Login

Stacking Code

public interface IBlog { string Dump(Stream consciousness); }

Assembly Versioning with Hg

Friday, 21 January, 2011 @ 6:10 PM < Adam Boddington
Tags: Building Neno, Mercurial

This is post #36 in the Building Neno series. Please click here for a description of the Building Neno project and instructions on how to access the source code for this post.

I've been playing with the new shiny hotness that is NuGet, and I've come to realise my assemblies could benefit from some proper versioning. NuGet doesn't specifically need it, but it would make sense to have the NuGet package version reflect the version of the primary assembly it contains.

Assembly Versioning

There's some great information on the three different types of assembly versions here. A summary follows, but feel free to skip ahead if you're more interested in the Hg specific code. ;)

Assembly Version

[assembly: AssemblyVersion("1.3.3.7")]

This is the version the CLR cares about. Assembly version is usually made of up four parts:

  • Major
  • Minor
  • Build
  • Revision

It's really up to the author(s) of the assembly what these numbers should mean, but there are a couple of things to keep in mind. Changing any part of the assembly version of a strongly named assembly will require referencers to recompile. So it makes sense to only change the assembly version when there are breaking changes in the code.

Assembly File Version

[assembly: AssemblyFileVersion("1.3.3.7")]

The CLR ignores the assembly file version, and it can be viewed in the properties of the assembly through the file system. If an assembly file version isn't provided, the assembly version is used instead. It's also composed of four numbers, and since the CLR ignores it, lots of folks take the opportunity to put an automated build number and/or a version control revision number somewhere in the assembly file version -- information that's helpful to know but shouldn't require a recompile when it changes.

Assembly Informational Version

[assembly: AssemblyInformationalVersion("zomg leet build!!!")]

Like the assembly file version, the assembly informational version is also ignored by the CLR and can be viewed in the properties of the assembly through the file system. Again like the assembly file version, if the assembly informational version isn't provided, the assembly version is used instead. The difference with this attribute is that it can take text, so folks typically use it for a more human readable assembly version.

My Versioning Plan

Here's my plan for versioning the assemblies in Moja and Neno.

Assembly Version

[assembly: AssemblyVersion("1.0.1000.0")]

The major and minor numbers are pretty traditional and I'll stay with the conventional wisdom. If it's a new round of development with only a few incremental changes, I'll increment the minor number. If it's a radical shift in the application with lots of new features, I'll increment the major number. The build number I'll use for indicating Alpha, Beta, Release Candidate, General Availability and patches. It's all a bit presumptuous -- I don't think my software will really go through the Alpha/Beta/RC/GA cycle, but I can see myself fixing bugs with patches. Having a way to force a recompile between GA and GA Patch 1, without having to increment the minor number, will be handy. Advancing the assembly version from 1.0.4000.0 to 1.0.4001.0 will let me do that.

The revision number will be left at 0, but could possibly be used for client specific patches/branches.

Assembly File Version

[assembly: AssemblyFileVersion("1.0.1000.62")]

This will be the same as the assembly version except the revision number will reflect the version control revision number for the source code. Being able to look at any previously compiled assembly and immediately pull the source code up for it is extremely valuable. When a client finds a bug in an assembly compiled two years ago, this will save everyone's butt.

While including a version control revision number works great for central version control systems like SVN, there's a bit of a gotcha with distributed version control systems like Mercurial. The local revision number isn't universal, which means the 62nd revision on my machine might not be the same as the 62nd revision on another machine. However, as long as the same machine is used for every build, it should still serve as an effective indicator of successive versions. To know the real Mercurial revision though, the changeset ID, a 160 bit unique identifier, needs to go somewhere too.

Assembly Informational Version

[assembly: AssemblyInformationalVersion("1.0 Alpha (62 75e7001fd211)")]

Here's where I can put in some text, which makes it the logical place for the Mercurial changeset ID -- as well as a translation of my build number to plain text. Also, if the build was made with uncommitted changes, I can use the Mercurial notation to indicate that by appending a plus sign to the revision number and changeset ID.

[assembly: AssemblyInformationalVersion("1.0 Alpha (62+ 75e7001fd211+)")]

Common Assembly Information

Rather than maintain different assembly versioning for every project in the solution, I plan to put the above information in a CommonAssemblyInfo.cs file in the first project, and then add the same file as a link to each subsequent project.

Add As Link

Every assembly built by the solution will have the same assembly versioning. It kind of makes sense since they're all related to each other, but a referencer of only one of the assemblies might be annoyed at having to recompile because a different assembly needed a patch... But hey, it can be split back out later if it's a repetitive problem.

Default File

The tool I plan to use to get information from Hg into CommonAssemblyInfo.cs is MSBuild Versioning, which uses a template with token replacement. It can be used from a standard MSBuild project file, but unfortunately it throws an error if Hg isn't installed. There's no option in the tool for specifying a default file, so I need to set that up manually.

This is an OSS project, I can't really assume everyone has Hg installed. If someone downloads the code as a zip file, I don't want their builds erroring out. Also, if I want to use AppHarbor via GitHub, I don't want their build failing either. AppHarbor says they will support Hg soon, however, so their builds may eventually work with MSBuild Versioning.

The CommonAssemblyInfo.cs file is kept out of version control since it changes after every commit. That means every build needs to generate something to go there. For that purpose, I've created a default file, CommonAssemblyInfo.default.cs, for the builds that run directly against the solution file. The default file has been added to version control and been given a BuildAction of None (found in file properties in Visual Studio 2010).

using System.Reflection;

[assembly: AssemblyCopyright("Copyright 2010 - 2011 Adam Boddington")]
[assembly: AssemblyFileVersion("1.0.1000.0")]
[assembly: AssemblyInformationalVersion("1.0 Alpha")]
[assembly: AssemblyVersion("1.0.1000.0")]

By editing StackingCode.Moja.csproj directly it's not hard to add some tasks to the BeforeBuild target to copy the default file to CommonAssemblyInfo.cs.

<PropertyGroup>
    <VersionPath1>Properties\CommonAssemblyInfo</VersionPath1>
    <VersionPath2>Version</VersionPath2>
</PropertyGroup>
<Target Name="BeforeBuild" Condition="'$(SkipDefaultVersion)' == '' Or !Exists('$(VersionPath1).cs') Or !Exists('$(VersionPath2).cs')">
    <Message Text="Applying Default Version" />
    <Copy SourceFiles="$(VersionPath1).default.cs" DestinationFiles="$(VersionPath1).cs" />
    <Copy SourceFiles="$(VersionPath2).default.cs" DestinationFiles="$(VersionPath2).cs" />
</Target>

There's a BeforeBuild target in every .csproj file. It just needs to be uncommented and modified. The property group is custom and needs to be inserted manually.

You might notice I actually have two templates that I'm managing in this way. More on the second one later. Also, the copy doesn't take place if the SkipDefaultVersioning property has been set to any value other than the default empty string. This gives me a way to turn it off when running the Hg template.

Hg Template

I've added another file, CommonAssemblyInfo.template.cs, to my project, also included in version control and given a BuildAction of None.

#define HG_IS_DIRTY_$DIRTY$
using System.Reflection;

[assembly: AssemblyCopyright("Copyright 2010 - 2011 Adam Boddington")]
[assembly: AssemblyFileVersion("1.0.1000.$REVNUM$")]
#if (HG_IS_DIRTY_1)
[assembly: AssemblyInformationalVersion("1.0 Alpha ($REVNUM$+ $REVID$+)")]
#else
[assembly: AssemblyInformationalVersion("1.0 Alpha ($REVNUM$ $REVID$)")]
#endif
[assembly: AssemblyVersion("1.0.1000.0")]

This is where the token replacement by MSBuild Versioning kicks in (check out their site for a complete list of tokens). At the time of writing this post, there's no easy way to translate the 0 or 1 $DIRTY$ is replaced with, but preprocessor directives can help with that.

To do the actual token replacement, I have a custom MSBuild project that I run manually. This will be my primary way of building the solution going forward.

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Build">
    <PropertyGroup>
        <SolutionName>StackingCode.Moja</SolutionName>
        <VersionPath1>Properties\CommonAssemblyInfo</VersionPath1>
        <VersionPath2>Version</VersionPath2>
    </PropertyGroup>
    <UsingTask TaskName="HgVersionFile" AssemblyFile="References/MSBuildVersioning/MSBuildVersioning.dll" />
    <Target Name="Version">
        <Message Text="Applying Hg Template Version" />
        <HgVersionFile TemplateFile="$(VersionPath1).template.cs" DestinationFile="$(VersionPath1).cs" />
        <HgVersionFile TemplateFile="$(VersionPath2).template.cs" DestinationFile="$(VersionPath2).cs" />
    </Target>
    <Target Name="Build" DependsOnTargets="Version">
        <MSBuild Projects="$(SolutionName).sln" Targets="Clean" Properties="Configuration=Release" />
        <MSBuild Projects="$(SolutionName).sln" Targets="Build" Properties="Configuration=Release;SkipDefaultVersion=True" />
    </Target>
</Project>

No rocket science here. The MSBuild Versioning library has a custom task called HgVersionFile. The UsingTask element tells MSBuild where to find it, and the HgVersionFile task requires the template filename and the destination filename. Easy as pie -- all the rocket science is inside the library -- kudos to Joe Daley.

Since this MSBuild project still defers to the actual solution file to do the build, the BeforeBuild target defined in the earlier .csproj file will run. Setting SkipDefaultVersion=True will tell it not to.

Here's the end result.

Properties

The Version Class

MSBuild Versioning isn't limited to assembly attributes either. Putting the same information into a Version class can be handy way to access the information programmatically without resorting to reflection.

#define HG_IS_DIRTY_$DIRTY$
using System;

namespace StackingCode.Moja
{
    public static class Version
    {
        static Version()
        {
            DateTime buildDate = DateTime.Parse("$UTCDATETIME$");
            buildDate = DateTime.SpecifyKind(buildDate, DateTimeKind.Utc);
            HgBuildDate = new DateTimeOffset(buildDate);
        }

        public static readonly string AssemblyFileVersion = "1.0.1000.$REVNUM$";
#if (HG_IS_DIRTY_1)
        public static readonly string AssemblyInformationalVersion = "1.0 Alpha ($REVNUM$+ $REVID$+)";
#else
        public static readonly string AssemblyInformationalVersion = "1.0 Alpha ($REVNUM$ $REVID$)";
#endif
        public static readonly string AssemblyVersion = "1.0.1000.0";

        public static readonly string HgBranch = "$BRANCH$";
        public static readonly DateTimeOffset HgBuildDate;
        public static readonly bool HgIsBuildDirty = $DIRTY$ == 1;
        public static readonly string HgRevisionId = "$REVID$";
        public static readonly int HgRevisionNumber = $REVNUM$;
        public static readonly string HgTags = "$TAGS$";
    }
}

Which lets me put some version information in the footer of my master page.

Footer

Handy, but I've got assembly versions defined in two files now, as well as on multiple lines. I may need to fix that up later.

EDIT: The next Building Neno post does just that. Check it out for a slightly cleaner solution.

There are 0 comments.


Comments

Leave a Comment

Please register or login to leave a comment.


Older
Lookups, Poppy's Way

Newer
Reasons Why I Avoid Entity Framework 4

Older
Lookups, Poppy's Way

Newer
Reasons Why I Avoid Entity Framework 4

browse with Pivot


About


Projects

Building Neno


RSS
Recent Posts

Codility Nitrogenium Challenge
OS X Lock
HACT '13
Codility Challenges
Priority Queue


Tags

Architecture (13)
ASP.NET (2)
ASP.NET MVC (13)
Brisbane Flood (1)
Building Neno (38)
C# (4)
Challenges (3)
Collections (1)
Communicator (1)
Concurrency Control (2)
Configuration (1)
CSS (5)
DataAnnotations (2)
Database (1)
DotNetOpenAuth (2)
Entity Framework (1)
FluentNHibernate (2)
Inversion of Control (5)
JavaScript (1)
jQuery (4)
Kata (2)
Linq (7)
Markdown (4)
Mercurial (5)
NHibernate (20)
Ninject (2)
OpenID (3)
OS X (1)
Pivot (6)
PowerShell (8)
Prettify (2)
RSS (1)
Spring (3)
SQL Server (5)
T-SQL (2)
Validation (2)
Vim (1)
Visual Studio (2)
Windows Forms (3)
Windows Service (1)


Archives


Powered by Neno, ASP.NET MVC, NHibernate, and small furry mammals. Copyright 2010 - 2011 Adam Boddington.
Version 1.0 Alpha (d9e7e4b68c07), Build Date Sunday, 30 January, 2011 @ 11:37 AM