Friday, September 12, 2014

Automatic Version Numbering

Here at the Unary Heap, I use C# and TortoiseSVN as my development stack of choice. In my last post about Visual Studio, I talked about how to set up a repository so that you can just check it out, and double-click a batch file in order to build it. This post will describe how to produce AssemblyVersion attributes from the Subversion repository number.

The Subversion command-line tools include 'svnversion.exe', a utility app specifically designed to be used as part of an automated build process to produce version numbers. However, it is not easily consumable by MSBuild in its raw form. Fortunately, the fine folks over at the MSBuild Community Tasks project have created an MSBuild task to call svnversion on a local workspace and parse its output for easy consumption as MSBuild properties. They also provide a text substitution task that can fill in a template file. Using these two tasks, I put together a task to produce the desired file:

<Target Name="GenerateBuildRevisionVersionFile" Condition="!Exists('$(SourceRoot)\BuildRevisionVersion.cs')">
 <MSBuild.Community.Tasks.Subversion.SvnVersion LocalPath="$(RepositoryRoot)">
  <Output TaskParameter="LowRevision" PropertyName="LowRevision" />
  <Output TaskParameter="HighRevision" PropertyName="HighRevision" />
  <Output TaskParameter="Modifications" PropertyName="HasLocalModifications" />
 </MSBuild.Community.Tasks.Subversion.SvnVersion>

 <Error Condition="$(LowRevision) != $(HighRevision)" Text="Working copy is not up-to-date Run 'svn update' to correct this."/>
 <Error Condition="'$(HasLocalModifications)' == 'True' AND '$(FailBuildIfLocallyModified)' != 'False'" Text="Working copy has local modifications." />

 <ItemGroup>
  <Tokens Include="svninfo">
   <ReplacementValue>$(HighRevision)</ReplacementValue>
  </Tokens>
 </ItemGroup>

 <MSBuild.Community.Tasks.TemplateFile OutputFilename="$(SourceRoot)\BuildRevisionVersion.cs" Template="$(SourceRoot)\BuildRevisionVersion.Template.cs" Tokens="@(Tokens)" />
</Target>

Given an input file 'BuildRevisionVersion.Template.cs', this task will produce 'BuildRevision.cs'. It will fail if the workspace is not all at a single revision, and it can be configured to fail if there are local modifications to the workspace, which is useful if you want to guarantee that you have produced a build from a pristine copy of your repository. It also has a condition to not run if the output already exists. (If the condition was absent, then each time it was run, it would force a rebuild of any projects that include the output file.) The output file is also included in the list of files to delete when running a 'clean' operation on the repository.

The template file itself is very simple:

static class BuildRevisionVersion
{
 public const string Build = "0";
 public const string Revision = "${svninfo}";
}

When you run the task, it replaces ${svninfo} with the workspace revision number.

My repository also contains another code file - 'VersionInformation.cs':

using System;
using System.Reflection;

[assembly: AssemblyVersion(VersionInformation.AssemblyVersionString)]
[assembly: AssemblyFileVersion(VersionInformation.AssemblyFileVersionString)]

static class VersionInformation
{
 public const string AssemblyVersionString = MajorMinorVersion.Major + "." + MajorMinorVersion.Minor + "." + BuildRevisionVersion.Build;
 public static Version AssemblyVersion { get { return new Version(AssemblyVersionString); } }

 public const string AssemblyFileVersionString = MajorMinorVersion.Major + "." + MajorMinorVersion.Minor + "." + BuildRevisionVersion.Build + "." + BuildRevisionVersion.Revision;
 public static Version AssemblyFileVersion { get { return new Version(AssemblyFileVersionString); } }
}

This file is what adds AssemblyVersion and AssemblyFileVersion attributes to an assembly, as well as providing convenience properties to access this information at run time as Version structs. This comes in handy for the output of console applications running with the /? switch, or in the About box of a GUI application.

Each .csproj file in my repository includes BuildRevision.cs and VersionInformation.cs, as links. (Remember to add as links, otherwise Visual Studio will create copies, and your output won't be dynamically-generated!) The classes they contain have the default scope of 'internal', so they won't collide with other assemblies referencing the same code file. Astute readers will notice that the code, as written, won't compile: there's no MajorMinorVersion class defined.

In my repository, each solution contains a 'MajorMinorVersion.cs' file, and each project in the solution includes it as a link. The file itself is very simple:

static class MajorMinorVersion
{
 public const string Major = "1";
 public const string Minor = "2";
}

This set-up allows me to declare the major/minor version numbers for each assembly on a per-solution basis, since I want all the assemblies in a solution to be at the same version, and I adhere to the DRY principle of Don't Repeat Yourself. Different solutions may be at different versions. However, every solution in my repository will have the same build/revision number when produced by a build.

No comments:

Post a Comment