Determining Build Date the hard way

April 14, 2005

One of the key diagnostic data points for any .NET assembly is "when was it built"? Until recently, I thought there were only two ways to suss this out:

  1. Check the filesystem date and time
  2. Derive the build date from the assembly version

The filesystem method has obvious limitations:

Function AssemblyLastWriteTime(ByVal a As Reflection.Assembly) As DateTime
  Try
    Return File.GetLastWriteTime(a.Location)
  Catch ex As Exception
    Return DateTime.MaxValue
  End Try
End Function

The version method, however, works quite well-- as long as developers don't deviate too far from the default .NET version string of <Assembly: AssemblyVersion("1.0.*")>

When specifying a version, you have to at least specify major. If you specify major and minor, you can specify an asterisk (*) for build. This will cause build to be equal to the number of days since January 1, 2000 local time, and for revision to be equal to the number of seconds since midnight local time, divided by 2.

If you specify major, minor, and build, you can specify an asterisk for revision. This will cause revision to be equal to the number of seconds since midnight local time, divided by 2.

Function AssemblyBuildDate(ByVal a As Reflection.Assembly, _
  Optional ByVal forceFileDate As Boolean = False) As DateTime

  Dim v As System.Version = a.GetName.Version
  Dim dt As DateTime

  If forceFileDate OrElse (v.Build < 730 Or v.Revision = 0) Then
    dt = AssemblyLastWriteTime(a)
  Else
    dt = New DateTime(2000, 1, 1, 0, 0, 0). _
      AddDays(v.Build). _
      AddSeconds(v.Revision * 2)
    If TimeZone.IsDaylightSavingTime(dt, _
      TimeZone.CurrentTimeZone.GetDaylightChanges(dt.Year)) Then
      dt = dt.AddHours(1)
    End If
    '-- sanity check
    If dt > DateTime.Now Or dt < New DateTime(2000, 1, 1, 0, 0, 0) Then
      dt = AssemblyLastWriteTime(a)
    End If
  End If

Return dt
End Function

Be careful when relying on version to predict build date in Visual Studio .NET. For some reason, the IDE does not update the build number every time you build a solution. Visual Studio only increments the build and revision number when the solution is closed and reopened. If you build fifty times throughout the day in the same solution, every single one of your builds will have the same version. Close and reopen that solution, though, and you'll get a new version immediately. Go figure.

Luckily, we don't have to settle for those two options. There's a third way to calculate build date that's much more reliable. Dustin Aleksiuk recently posted a clever blog entry describing how to retrieve the embedded linker timestamp from the IMAGE_FILE_HEADER section of the Portable Executable header:

Function RetrieveLinkerTimestamp(ByVal filePath As String) As DateTime
  Const PeHeaderOffset As Integer = 60
  Const LinkerTimestampOffset As Integer = 8

  Dim b(2047) As Byte
  Dim s As Stream
  Try
    s = New FileStream(filePath, FileMode.Open, FileAccess.Read)
    s.Read(b, 0, 2048)
  Finally
    If Not s Is Nothing Then s.Close()
  End Try
  
  Dim i As Integer = BitConverter.ToInt32(b, PeHeaderOffset)
  
  Dim SecondsSince1970 As Integer = BitConverter.ToInt32(b, i + LinkerTimestampOffset)
  Dim dt As New DateTime(1970, 1, 1, 0, 0, 0)
  dt = dt.AddSeconds(SecondsSince1970)
  dt = dt.AddHours(TimeZone.CurrentTimeZone.GetUtcOffset(dt).Hours)
  Return dt
End Function

When I ran Dustin's code for the first time, I wondered why the dates and minutes were correct, but the hours were consistently off by four. Even I can figure out GMT/UTC issues when they practically slap me in the face. I emailed Dustin to ask him what he thought, and as it turns out, Dustin lives in GMT-- that's the ultimate "it runs on my machine"! Sure does make those pesky mental IIS logfile date conversions easier, too.. ;)

Posted by Jeff Atwood
24 Comments

Thanks for that! I'll correct and update that post over the next few days.

I've read your blog before. Nice to see stuff about VB.NET. I'm not the biggest VB.NET fan, but I seem to end up using it a lot and it's not going away. :)

Regards,
Dustin

Dustin Aleksiuk on April 15, 2005 5:32 AM

That whole "number of days" and "number of seconds" thing made me nervous, even without having to open and close the IDE. Even though some people passionately explained to me why it's a good thing, those numbers seem basically random and make it hard to know what version you are in fact looking at. So I (and lots of others) wrote a little VS macro to update the build number every time I do a build, and I update the other three according to the bigness of the change. Then the file version is pretty useful, I think.

Great blog, btw.

Josh on April 15, 2005 6:02 AM

Though, upon considering your post, I realize that my method doesn't tell you anything about when the file was actually built. I bet another macro could be used to take this string and write it as a resource in the executable. Probably not as sexy as Dustin's approach though.

Josh on April 15, 2005 6:04 AM

So I (and lots of others) wrote a little VS macro to update the build number every time I do a build

Yes, that's certainly logical. But it begs the question: why didn't Microsoft do it this way? Oversight? Or did they just want to ensure the build and revision number are different every time in the absence of any real metadata provided by the developer?

My method doesn't tell you anything about when the file was actually built.

Right, and build # is kind of a meaningless number anyway. Does anyone remember the build number of the first release of NT 4.0? of XP? The *date* of the build is much more useful information than how many times the developers on the project happened to press the F5 key before it was packaged into a box and shipped..

Jeff Atwood on April 15, 2005 10:49 AM

Given that a 32 bit int from 1970 will expire in 2038, which is coming up soon, I think I'd prefer the assembly version method instead.

Marc

Marc Clifton on August 25, 2006 3:12 AM

Thanks for that great solution. I converted it to c# (with minor mod), listed here if anyone wants it (sorry the formatting looks so bad :)

private DateTime RetrieveLinkerTimestamp()
{
string filePath = System.Reflection.Assembly.GetCallingAssembly().Location;
const int c_PeHeaderOffset = 60;
const int c_LinkerTimestampOffset = 8;
byte[] b = new byte[2048];
System.IO.Stream s = null;

try
{
s = new System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read);
s.Read(b, 0, 2048);
}
finally
{
if (s != null)
{
s.Close();
}
}

int i = System.BitConverter.ToInt32(b, c_PeHeaderOffset);
int secondsSince1970 = System.BitConverter.ToInt32(b, i + c_LinkerTimestampOffset);
DateTime dt = new DateTime(1970, 1, 1, 0, 0, 0);
dt = dt.AddSeconds(secondsSince1970);
dt = dt.AddHours(TimeZone.CurrentTimeZone.GetUtcOffset(dt).Hours);
return dt;
}

Joe Spivey on October 20, 2006 3:03 AM

Thanks for the Code
Regards
Joseph

Joseph on July 4, 2007 6:39 AM

txs that helped!

fmeri on July 5, 2007 6:39 AM

Best site on that topic I found in the web.
Thanks for the code.

Kay-Ulrich on October 19, 2007 8:13 AM

The build hour and minutes not changing on each release .NET build compile caught me out. Norton AV prompted every time the binary changed, so I'd expected version number to change. Open/Close IDE – nice tip!

Andy on January 14, 2008 9:51 AM

Thanks Joe, your code worked perfectly.

Echilon on April 21, 2008 7:24 AM

1.0.* doesn't work it show 1.0.* or error. Article is useless.

Andy on June 13, 2008 11:27 AM

Andy (above) thinks he's being clever, but in fact his curt little 'error report' has robbed him of any chance of help. Well done you. It works perfectly for me, although it did take a few tries with asterisks and blanks around different places to get it going. (I'm on C# Express 2008) This is what patience and being polite gets you.

Thanks, Joe.

Andrew on July 8, 2008 4:10 AM

Thanks Jeff too -- seems only fair!

Andrew on July 8, 2008 4:10 AM

Thank you so much Joe. I just added your method in my about box and sure enough, it compiled and worked right immediately!

Sansay on August 21, 2008 1:51 PM

nice try but
Function RetrieveLinkerTimestamp() As DateTime

doesn't change with each build cycle, :

Const PeHeaderOffset As Integer = 60
Const LinkerTimestampOffset As Integer = 8
Dim filePath = System.Reflection.Assembly.GetCallingAssembly().Location

Dim b(2047) As Byte
Dim s As Stream
Try
s = New FileStream(filePath, FileMode.Open, FileAccess.Read)
s.Read(b, 0, 2048)
Finally
If Not s Is Nothing Then s.Close()
End Try

Dim i As Integer = BitConverter.ToInt32(b, PeHeaderOffset)

Dim SecondsSince1970 As Integer = BitConverter.ToInt32(b, i + LinkerTimestampOffset)
Dim dt As New DateTime(1970, 1, 1, 0, 0, 0)
dt = dt.AddSeconds(SecondsSince1970)
dt = dt.AddHours(TimeZone.CurrentTimeZone.GetUtcOffset(dt).Hours)
Return dt
End Function

Todd on September 8, 2008 2:49 AM

only way so far is to exit and reload/restart vs.

what a pain.

Todd on September 8, 2008 2:50 AM

Thank you so much Joe. I just used your C# code and it worked like a charm. Our busy testing environment needed to have the latest version with latest build date set.

Chasan Chouse on November 4, 2008 11:50 AM

Could someone please explain how/why this works?
I see we get 4 bytes as int from the 60th byte in the PE and assign it to i.
Which I assuem is the time.
We then get prosumeably the seconds since 1970, which is found at ...
(i plus the constant 8)th byte in the PE.

So this means that the time we get from the PE is always in a different possition, which is dependant on the variable i (according to the code).

Or have I missed something here?

Kim on February 19, 2009 1:40 AM

Ok, i is not the time, it's 128.
Not sure of the relevance of 128 though.

Kim on February 19, 2009 1:52 AM

Hello Kim,

The Exe file format dates back to the early 80s (before Windows) to an operating system called DOS. That OS did not anticipate Windows or even later versions of DOS with long file names. subsequent versions had to be compatible with programs written for the earlier DOS version. For this reason the exe file format has evolved and includes sections that are fixed and only make sense for a 16 bit DOS application.
The first offset (60 or 3Ch) is an offset to an offset to the Windows file header.
This web URL will explain the file format in general terms http://support.microsoft.com/kb/65122

-
Kevin

Kevin on June 15, 2009 2:23 AM

Hello Kim,

The Exe file format dates back to the early 80s (before Windows) to an operating system called DOS. That OS did not anticipate Windows or even later versions of DOS with long file names. subsequent versions had to be compatible with programs written for the earlier DOS version. For this reason the exe file format has evolved and includes sections that are fixed and only make sense for a 16 bit DOS application.
The first offset (60 or 3Ch) is an offset to an offset to the Windows file header.
This web URL will explain the file format in general terms http://support.microsoft.com/kb/65122

-
Kevin

Kevin on June 15, 2009 2:25 AM

Thanks Jeff,
It's hard to believe in this day and age that this is the best way to do things in .Net but it seems to still be the valid option.

Scottie on July 22, 2009 11:53 AM

Thanks for this code. It helped me a lot!!! Regards, me

Halcyon on February 6, 2010 9:30 PM

The comments to this entry are closed.