Delphi compile timestamp

It could be useful to know the build date of the executable. In most situations this is the file’s timestamp.
It is quite simple to read it out in TDateTime format:

ExeDate := FileDateToDateTime(FileAge(ParamStr(0)));

However, there are situations when the file’s timestamp is altered and the build date is lost.

By introduction of D2006, the build date is stored in the PE (Portable Executable format) header.
Before Delphi 2006, the build date is stored in the Resource Directory Table of PE.

My first approach was to update the timestamp in the PE header for old Delphi versions. I planned to create an IDE plugin which calls the update at the AfterCompile event. The timestamp read-out would be simpler and would not consider the Delphi version. Automatic daily builds don’t use the IDE, so a separate updater tool is required. It seemed unnecessarily complex and I didn’t find good examples to alter the PE header.

On newer Delphi compilers, to get the build date for the currently running executable is easy:

function GetLinkerTimestamp: TDateTime;
begin
  Result := PImageNtHeaders(HInstance + Cardinal(PImageDosHeader(HInstance)^._lfanew))^.FileHeader.TimeDateStamp / SecsPerDay + UnixDateDelta;
end;

For other executables it is still simple:

function GetLinkerTimeStamp(const Filename: AnsiString): TDateTime;
var
  LoadedImage: PLoadedImage;
begin
  LoadedImage := ImageLoad(PAnsiChar(Filename), nil);
  Result := LoadedImage.FileHeader.FileHeader.TimeDateStamp / SecsPerDay + UnixDateDelta;
  ImageUnload(LoadedImage);
end;

Please note that, this is the UTC time (or GMT). To get the local time, convert it (Delphi XE):

function ConvertToLocalTime(UTC: TDateTime): TDateTime;
begin
  Result := TTimeZone.Local.ToLocalTime(UTC);
end;

Please also note that, ImageLoad is non-Unicode!

The PE header’s timestamp is always 0x2A425E19 (1992.06.19 22:22:17) on older Delphi versions. This magic number can be used to distinguish between old and new versions.

On older Delphi compilers, to get the build date it is a bit tricky. We have to find the Directory Entry Resource table and read out its TimeDateStamp field. I found even a simple console application has a resource directory so this solution seems generally acceptable.

There’s an ImageDirectoryEntryToData function in ImageHlp.dll which helps to find the Resource Directory Entry in a Loaded Image. For the currently running executable the LoadedImage’s base address is pointed by HInstance.

The IMAGE_RESOURCE_DIRECTORY structure is not defined in the ImageHlp.pas, so let’s define a new type:

type
  PIMAGE_RESOURCE_DIRECTORY = ^IMAGE_RESOURCE_DIRECTORY;
  IMAGE_RESOURCE_DIRECTORY = packed record
    Characteristics: DWORD;
    TimeDateStamp: DWORD;
    MajorVersion: WORD;
    MinorVersion: WORD;
    NumberOfNamedEntries: WORD;
    NumberOfIdEntries: WORD;
  end;
function GetLinkerTimestamp: TDateTime;
var
  ImgResDir: PIMAGE_RESOURCE_DIRECTORY;
  Size: Cardinal;
begin
  Result := 0;
  ImgResDir := ImageDirectoryEntryToData(Pointer(HInstance), True, IMAGE_DIRECTORY_ENTRY_RESOURCE, Size);
  if ImgResDir <> nil then
    Result := FileDateToDateTime(ImgResDir.TimeDateStamp);
end;

This last version reads out the resource directory’s timestamp from executables:

function GetLinkerTimestamp(const Filename: AnsiString): TDateTime;
var
  LoadedImage: PLoadedImage;
  Size: Cardinal;
  ImgResDir: PIMAGE_RESOURCE_DIRECTORY;
begin
  Result := 0;
  LoadedImage := ImageLoad(PAnsiChar(Filename), nil);
  if LoadedImage <> nil then
  begin
    ImgResDir := ImageDirectoryEntryToData(LoadedImage.MappedAddress, False, IMAGE_DIRECTORY_ENTRY_RESOURCE, Size);
    if ImgResDir <> nil then
      Result := FileDateToDateTime(ImgResDir.TimeDateStamp);
    ImageUnload(LoadedImage);
  end;
end;

This holds the local time, so no need to convert it.

Leave a Reply

Your email address will not be published. Required fields are marked *