Versioning the Microsoft way... with NAnt

I was inspired by a recent blog entry by Jeff Atwood here about how Microsoft versions their products and how the build number is significant. I thought it would be good to post a walkthrough of how to build your own versioning system ala Microsoft but using NAnt. I'm sure some budding geek out there could convert this to MSBuild, but you know my love of that tool so NAnt it is.

First off, NAnt has a great facility for generating that AssemblyInfo.cs file that every project has. It's the asminfo task and basically looks like this:

<?xml version="1.0"?>
<project name="Test" default="UpdateAssemblyInfo">
    <target name="UpdateAssemblyInfo">
        <asminfo output="AssemblyInfo.cs" language="CSharp">
            <imports>
                <import namespace="System.Reflection" />
                <import namespace="System.Runtime.InteropServices" />
            </imports>
            <attributes>
                <attribute type="AssemblyTitleAttribute" value="ClassLibrary1" />
                <attribute type="AssemblyDescriptionAttribute" value="" />
                <attribute type="AssemblyConfigurationAttribute" value="" />
                <attribute type="AssemblyCompanyAttribute" value="" />
                <attribute type="AssemblyProductAttribute" value="ClassLibrary1" />
                <attribute type="AssemblyCopyrightAttribute" value="Copyright (c) 2007" />
                <attribute type="AssemblyTrademarkAttribute" value="" />
                <attribute type="AssemblyCultureAttribute" value="" />
 
                <attribute type="ComVisibleAttribute" value="false" />
 
                <attribute type="GuidAttribute" value="f98c8021-fbf1-44ff-a484-946152cefdb8" />
 
                <attribute type="AssemblyVersionAttribute" value="1.0.0.0" />
                <attribute type="AssemblyFileVersionAttribute" value="1.0.0.0" />
            </attributes>
        </asminfo>
    </target>
</project>

This will product a default AssemblyInfo.cs file that looks like this:

using System.Reflection;
using System.Runtime.InteropServices;
 
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:2.0.50727.42
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
 
[assembly: AssemblyTitleAttribute("ClassLibrary1")]
[assembly: AssemblyDescriptionAttribute("")]
[assembly: AssemblyConfigurationAttribute("")]
[assembly: AssemblyCompanyAttribute("")]
[assembly: AssemblyProductAttribute("ClassLibrary1")]
[assembly: AssemblyCopyrightAttribute("Copyright (c) 2007")]
[assembly: AssemblyTrademarkAttribute("")]
[assembly: AssemblyCultureAttribute("")]
[assembly: ComVisibleAttribute(false)]
[assembly: GuidAttribute("f98c8021-fbf1-44ff-a484-946152cefdb8")]
[assembly: AssemblyVersionAttribute("1.0.0.0")]
[assembly: AssemblyFileVersionAttribute("1.0.0.0")]

Notice however a few things. First is the Guid. We had to hard code that which might be okay, but lets dig into NAnt scripting by replacing it with a real Guid. NAnt also has the ability to let you write embedded code (C#, VB.NET, etc.) via the <script> task, so let's write a small task to do that. We'll just have it generate a new Guid and set a new custom property in the NAnt script that we'll use in our asminfo task. Create a property in the NAnt script to hold our Guid:

<property name="project.guid" value="f98c8021-fbf1-44ff-a484-946152cefdb8" />

Then use that property in our GuidAttribute:

<attribute type="GuidAttribute" value="${project.guid}" />

Finally here's the task to generate a Guid via NAnt (make the default UpdateAssemblyInfo task dependent on this one):

<target name="CreateUniqueGuid">
    <script language="C#">
        <code>
            <![CDATA[
                public static void ScriptMain(Project project) {
                    project.Properties["project.guid"] = Guid.NewGuid().ToString();
                }
            ]]>
        </code>
    </script>
</target>

Great. We now have a NAnt script that will generate a new version file with a unique Guid everytime. Next we want to tackle the versioning issue.

As described by Jensen Harris here, the Microsoft Office scheme is pretty simple:

  • Take the year in which a project started. For Office "12", that was 2003.
  • Call January of that year "Month 1."
  • The first two digits of the build number are the number of months since "Month 1."
  • The last two digits are the day of that month.

Using this we'll need to setup a couple of properties. One is to hold the year the project starts, the other is the build version we want to set:

<property name="project.year" value="2003" />
<property name="build.version" value="1.0.0.0" />

Now we could write a lot of NAnt code as there are functions to manipulate dates, but it's much easier using the <script> task and some C#. Here's the NAnt task to generate the build number using the Microsoft Office approach:

<target name="GenerateBuildNumber">
    <script language="C#">
        <imports>
            <import name="System.Globalization" />
            <import name="System.Threading" />
        </imports>
        <code>
            <![CDATA[
                public static void ScriptMain(Project project) {
                    Version version = new Version(project.Properties["build.version"]);
                    int major = version.Major;
                    int minor = version.Minor;
                    int build = version.Build;
                    int revision = version.Revision;
 
                    int startYear = Convert.ToInt32(project.Properties["project.year"]);
                    DateTime start = new DateTime(startYear, 1, 1);
                    Calendar calendar = Thread.CurrentThread.CurrentCulture.Calendar;
                    int months = ((calendar.GetYear(DateTime.Today)
                        - calendar.GetYear(start)) * 12)
                        + calendar.GetMonth(DateTime.Today)
                        - calendar.GetMonth(start);
                    int day = DateTime.Now.Day;
                    build = (months * 100) + day;
 
                    version = new Version(major, minor, build, revision);
                    project.Properties["build.version"] = version.ToString();
                }
            ]]>
        </code>
    </script>

We get the version in the NAnt script as a starter (since we're only replacing the build number) and then assign values to it (they're read-only in .NET). Then this is written back out to the property as a string.

If this is run today (February 17, 2007) it's been 49 months since the start of 2003 and today is the 17th day. So the build number is 4917. 

Here's the finaly output from this NAnt script:

using System.Reflection;
using System.Runtime.InteropServices;
 
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:2.0.50727.42
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
 
[assembly: AssemblyTitleAttribute("ClassLibrary1")]
[assembly: AssemblyDescriptionAttribute("")]
[assembly: AssemblyConfigurationAttribute("")]
[assembly: AssemblyCompanyAttribute("")]
[assembly: AssemblyProductAttribute("ClassLibrary1")]
[assembly: AssemblyCopyrightAttribute("Copyright (c) 2007")]
[assembly: AssemblyTrademarkAttribute("")]
[assembly: AssemblyCultureAttribute("")]
[assembly: ComVisibleAttribute(false)]
[assembly: GuidAttribute("a6e7ff79-63ba-443f-8bc3-0c4b43f43ffe")]
[assembly: AssemblyVersionAttribute("1.0.4917.0")]
[assembly: AssemblyFileVersionAttribute("1.0.4917.0")]
 

And here's the full NAnt script:

    1 <?xml version="1.0"?>
    2 <project name="Test" default="UpdateAssemblyInfo">
    3 
    4     <property name="project.guid" value="f98c8021-fbf1-44ff-a484-946152cefdb8" />
    5     <property name="project.year" value="2003" />
    6     <property name="build.version" value="1.0.0.0" />
    7 
    8     <target name="UpdateAssemblyInfo" depends="CreateUniqueGuid, GenerateBuildNumber">
    9         <asminfo output="AssemblyInfo.cs" language="CSharp">
   10             <imports>
   11                 <import namespace="System.Reflection" />
   12                 <import namespace="System.Runtime.InteropServices" />
   13             </imports>
   14             <attributes>
   15                 <attribute type="AssemblyTitleAttribute" value="ClassLibrary1" />
   16                 <attribute type="AssemblyDescriptionAttribute" value="" />
   17                 <attribute type="AssemblyConfigurationAttribute" value="" />
   18                 <attribute type="AssemblyCompanyAttribute" value="" />
   19                 <attribute type="AssemblyProductAttribute" value="ClassLibrary1" />
   20                 <attribute type="AssemblyCopyrightAttribute" value="Copyright (c) 2007" />
   21                 <attribute type="AssemblyTrademarkAttribute" value="" />
   22                 <attribute type="AssemblyCultureAttribute" value="" />
   23 
   24                 <attribute type="ComVisibleAttribute" value="false" />
   25 
   26                 <attribute type="GuidAttribute" value="${project.guid}" />
   27 
   28                 <attribute type="AssemblyVersionAttribute" value="${build.version}" />
   29                 <attribute type="AssemblyFileVersionAttribute" value="${build.version}" />
   30             </attributes>
   31         </asminfo>
   32     </target>
   33 
   34     <target name="CreateUniqueGuid">
   35         <script language="C#">
   36             <code>
   37                 <![CDATA[
   38                     public static void ScriptMain(Project project) {
   39                         project.Properties["project.guid"] = Guid.NewGuid().ToString();
   40                     }
   41                 ]]>
   42             </code>
   43         </script>
   44     </target>
   45 
   46     <target name="GenerateBuildNumber">
   47         <script language="C#">
   48             <imports>
   49                 <import name="System.Globalization" />
   50                 <import name="System.Threading" />
   51             </imports>
   52             <code>
   53                 <![CDATA[
   54                     public static void ScriptMain(Project project) {
   55                         Version version = new Version(project.Properties["build.version"]);
   56                         int major = version.Major;
   57                         int minor = version.Minor;
   58                         int build = version.Build;
   59                         int revision = version.Revision;
   60 
   61                         int startYear = Convert.ToInt32(project.Properties["project.year"]);
   62                         DateTime start = new DateTime(startYear, 1, 1);
   63                         Calendar calendar = Thread.CurrentThread.CurrentCulture.Calendar;
   64                         int months = ((calendar.GetYear(DateTime.Today)
   65                             - calendar.GetYear(start)) * 12)
   66                             + calendar.GetMonth(DateTime.Today)
   67                             - calendar.GetMonth(start);
   68                         int day = DateTime.Now.Day;
   69                         build = (months * 100) + day;
   70 
   71                         version = new Version(major, minor, build, revision);
   72                         project.Properties["build.version"] = version.ToString();
   73                     }
   74                 ]]>
   75             </code>
   76         </script>
   77     </target>
   78 
   79 </project>

Enjoy!

2 Comments

  • Nice post, but the NAnt snippets are getting cut off along the right edge.

    I think you don't want to recreate the Guid value with each build as this value is used when you are creating an assembly that is also being exported to COM.

  • @Scott: Thanks for the info, I'll look at reposting the code (copy as HTML isn't working quite right). As for regenerating the Guid, that's optional and I agree you generally wouldn't do it everytime but it was just an example of how to do it.

Comments have been disabled for this content.