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!

Published Saturday, February 17, 2007 1:58 PM by Bil Simser

Comments

# re: Versioning the Microsoft way... with NAnt

Saturday, February 17, 2007 11:47 PM by Scott

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.

# re: Versioning the Microsoft way... with NAnt

Sunday, February 18, 2007 11:17 AM by Bil Simser

@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.

# re: Versioning the Microsoft way... with NAnt

Wednesday, February 21, 2007 6:12 PM by David Keaveny

You might be interested to know that I posted a GuidTask task to the NAntContrib project in June 2006 to facilitate the creation of GUIDs (I needed it for some WiX integration) - see http://www.mail-archive.com/nantcontrib-developer@lists.sourceforge.net/msg01507.html

Feel free to drop it in place of your C# scripts :-) Who knows, maybe the project owners will actually consider including it in the main builds...

# re: Versioning the Microsoft way... with NAnt

Tuesday, February 27, 2007 7:51 AM by Simon Phillips

Last Year I put together a Cruise Control.Net labelling module, which uses the same logic to create the version number.

I did start this as a nAnt script, but I found that I was creating project builds with multiple dlls, which I was needing to have the same version number.

As I was using CC.Net to schedule and run my nAnt build scripts this was the better solution for me.

http://nimtug.org/blogs/simon/articles/CCNet_MS_Project_Labeller.aspx