[Estimated Reading Time: 8 minutes]

A key step in the evolution of duget is an auto-update mechanism for the duget client itself. Going hand-in-hand with that is a need for reliable identification of the version of the client in use and any newer versions that may be available. In taking this step I have run into a minor problem caused by the constraints in place for VERSIONINFO resources of Windows executables.

For duget packages themselves I have adopted Semantic Versioning, but the more traditional Windows version numbers in a VERSIONINFO resource has a number of restrictions which make them incompatible with the Semantic Version scheme (as discussed in some depth in this article).

These constraints extend also (with some variations) to Assembly versioning and MSI installers, so this is not solely a ‘legacy’ problem.

Put simply, a VERSIONINFO version number is limited to an entirely numeric format with four components to the version. There is no formal specification for what these four values represent, only different conventions, but for the purposes of this post we’ll use:

Major.Minor.Revision.Build

Each of these values is limited to a maximum value as follows:

255.255.65534.65534

Semantic Versioning by contrast supports only three numeric components, referred to as:

Major.Minor.Patch

Semantic Versioning also caters for zero or more alpha-numeric identifiers and a further zero or more items of build meta-data.

Major.Minor.Patch-<identifiers>+<build meta-data>

Identifiers are used to identify pre-release versions and are significant in determining version precedence. Build Meta-Data is entirely documentary in nature and plays no part in version precedence.

I am not currently generating or using Build Meta-Data at all so we won’t be mentioning that again in this post.


As mentioned in earlier posts, I am using GitVersion to automatically derive a Semantic Version for my packages based on a combination of a specified version in a GitVersion.yml configuration in each project combined with the branch and commit history in the repository at the time of the build.

For duget packages this works well as the Semantic Version is simply recorded as meta-data in the .duspec manifest for the package, which is a simple JSON file, as well as in the filename of the package itself.

When building an executable ideally the Semantic Version would carry forward into the VERSIONINFO of the exe produced by the build. But this is where we run into a problem.

A pre-release build of the duget exe might have a Semantic Version similar to:

0.1.0-alpha.15

The first three fields are no problem as we can simply use these for Major.Minor.Revision. But there is no way to represent the alpha-numeric pre-release identifier in the numeric Build field of a VERSIONINFO version number.

What to do ?

Option #1: Ignore the Label ?

Could we simply ignore the alpha label in the pre-release identifier, retain only the build number (the .15 part of the identifier) and use this for the Build of the VERSIONFINO ? This would mean that our pre-release VERSIONINFO for 0.1.0-alpha.15would be:

0.1.0.15

The problem with this is that once the 0.1.0 is released, that pre-release identifier is dropped. For Semantic Versioning this is handled by the rules of precedence which state that a version with an identifier has lower precedence than the same version with no identifiers.

That is, 0.1.0 is a more current version than 0.1.0-alpha.15. This is fine as the pre-release label clearly identifies it as such. The alpha.15 build of 0.1.0 is not yet the released version of 0.1.0. This makes perfect sense.

But applying the same reasoning to a label-less VERSIONINFO equivalent leads to the clearly counter-intuitive outcome that 0.1.0 is a more current version than 0.1.0.15!

Option #2: Subvert Expectations

This isn’t so much a second option as a way of lending legitimacy to the first (or trying to).

If 0.1.0-beta.15 is a pre-release version then we could take the position that it is a pre-release not of 0.1.0 but of some later, future version as yet to be decided. That is, the next release version might be 0.1.1 and so 0.1.0.15 is a valid earlier version, identified as a pre-release by the non-zero Build field. All release versions would have a Build of 0. Any version with a non-zero Build number is a pre-release of some future version.

This has some obvious drawbacks, not least of which is the fact that there is no way to tell from 0.1.0.15 alone which subsequent version it is a pre-release version of. It might be 0.1.1 or 0.2.0 or even 1.0.0 etc.

No, that’s not going to fly.

Option #3: Use StringFileInfo for Semantic Version

Anyone intimately familiar with the VERSIONINFO resource will have noticed that I have so far not mentioned the FileVersion and ProductVersion values held in the StringFileInfo block, maintained in addition to the FILEVERSION and PRODUCTVERSION numeric fields.

These are simple string values (promising!) and the Microsoft documentation clearly states (with an example provided of “5.00.RC2”) that both of these can hold any arbitrary string! Unfortunately, when examining the version properties of a file in Windows Explorer, either the Windows Explorer dialog is using the FILEVERSION value in preference over the FileVersion string, or the FILEVERSION value actually ignores any non-numeric content that you try to put in this field.

I really should check which it is, just to be sure, but the fact remains that what people will see when they inspect the Details of the file in Windows Explorer is not particularly helpful in this case.

That is, a subtle attempt to reformat the version into a format ‘compatible’ with the 4-dotted fields expected in a VERSIONINFO, e.g:

VALUE "FileVersion", "0.1.0.beta-15"

Results in an actual File Version value of… 0.1.0.15 being displayed.

The good news is that the ProductVersion property does display any arbitrary string provided.

This led me to the approach I eventually adopted.


In short, I decided that Semantic Versioning would be retained and maintained in the ProductVersion string but not FileVersion. I still needed (or rather, ‘wanted’) a unique, incrementing version that I could use in the other version fields that would not be confusing.

Remember that the ‘build number’ derived from the GitVersion applies only to pre-release builds which presents two problems with using that.

  1. It will reset when the major, minor or patch numbers are incremented
  2. It does not get derived at all for release builds

What I wanted was a constantly increasing number that would be reliably and consistently generated regardless of the branch on which the build was being performed (be it a pre-release build or master release build).

However, assuming that this value would be used in either the Revision or Build fields of the VERSIONINFO it also could not exceed the value 65534.

I eventually settled on using not one but two such numbers.

Since only the Major and Minor fields are strictly in ‘common’ between Semantic Version and VERSIONINFO I decided that these would also be the only values carried forward from the Semantic Version into the VERSIONINFO. I would derive the Revision and Build values independently of the Major and Minor version. using a constantly incrementing scheme.

There are numerous solutions to the constantly incrementing build number problem for Azure DevOps. The approach I decided to use is derived from a value that increases intrinsically and inevitably over time: Time itself.

For Revision I would use a days counter. That is, this field will identify the date on which the build was produced, calculated as the number of days since 1st Jan 2000. Why that date ? Why not ? 🙂

For Build I would use a second counter. That is, this field identifies the time of day at which the build was produced. But since there are 86400 seconds in a day, this number is actually divided by 2 in order to remain within the 65534 limit of the field.

This gives me a ‘resolution’ for my builds of only 2 seconds, but I don’t anticipate ever having a problem caused by my builds completing in under 2 seconds! The days calculation gives me ~160 years in which to figure out a solution to the problem of that number ‘running out’, which should be enough time I think.


Now all that remains is to get this into my build pipeline. Bearing in mind that I am using the command line compiler in my builds, the trick of referencing a .rc file and having the compiler automatically invoke the resource compiler to produce a .res file won’t work. Even if it did, the header files necessary for compiling a VERSIONINFO resource are not provided with Delphi.

Fortunately my self-hosted build machine already has a Windows 10 SDK installed, so I can use the resource compiler from that directly.

In my project .dpr I reference a versioninfo.resfile. A version of this file is maintained in the repository with properties that identify any build as a “Development” version.

This isn’t perfect. Ideally it would at least also identify the build as a development build of the Major and Minor version from the GitVersion.yml configuration. This is something I may turn my attention to at some point, if it becomes a problem. But until then there are bigger fish to fry.

Alongside the versioninfo.res file is a versioninfo.rc file. This is the VERSIONINFO resource script containing most of the meta-data needed for the version info itself:

//
// Include the necessary resources
//
#include <winver.h>
#include <versioninfo.h>

#define VER_FILENAME     "duget.exe"
#define VER_PRODUCTNAME  "Duget Package Manager for Delphi"
#define VER_INTERNALNAME "Duget CLI"
#define VER_COMPANYNAME  "Deltics (Jolyon Direnko-Smith)"
#define VER_COMMENTS     "For more information visit https://duget.org"
#define VER_DESCRIPTION  "Duget CLI Client Executable"
#define VER_COPYRIGHT    "(c)2019 Jolyon Direnko-Smith"

#ifdef RC_INVOKED
	VS_VERSION_INFO VERSIONINFO
	FILEVERSION             VER_MAJOR,VER_MINOR,VER_TIMESTAMP_DAYS,VER_TIMESTAMP_SECONDS
	PRODUCTVERSION          VER_MAJOR,VER_MINOR,0,0
	FILEFLAGSMASK           VS_FFI_FILEFLAGSMASK
	FILEFLAGS               0
	FILEOS                  VOS_NT
	FILETYPE                VFT_APP
	FILESUBTYPE             0
	BEGIN
		BLOCK "StringFileInfo"
		BEGIN
			BLOCK "040904B0"
	        BEGIN
			VALUE "Comments",         VER_COMMENTS
			VALUE "CompanyName",      VER_COMPANYNAME
			VALUE "FileDescription",  VER_DESCRIPTION
			VALUE "FileVersion",      VER_FILEVERSION
			VALUE "InternalName",     VER_INTERNALNAME
			VALUE "LegalCopyright",   VER_COPYRIGHT
			VALUE "OriginalFilename", VER_FILENAME
			VALUE "ProductName",      VER_PRODUCTNAME
			VALUE "ProductVersion",	  VER_PRODUCTVERSION
			END
		END
		BLOCK "VarFileInfo"
		BEGIN
			VALUE "Translation", 0x0409,1200
		END
	END
#endif

Some key information is missing however, and this is expected to be provided in the versioninfo.h file that is #included at the top of the file. This file is created at build time by the build pipeline:

steps:
- template: gitversion.yml@templates

- powershell: |
    $baseDate = [datetime]"01/01/2000"
    $baseTime = [datetime]"00:00:00"
    $now = Get-Date

    $daysInterval = New-TimeSpan -Start $baseDate -End $now
    $secsInterval = New-TimeSpan -Start $baseTime -End $now
    $days = $daysInterval.Days
    $secs = [int]($secsInterval.TotalSeconds / 2)

    $verMajor = $ENV:GitVersionMajor
    $verMinor = $ENV:GitVersionMinor
    $gitVersion = $ENV:GitVersion

    Remove-Item versioninfo.h | Out-Null
    New-Item versioninfo.h -ItemType file | Out-Null
    Add-Content versioninfo.h "#define VER_MAJOR $verMajor"
    Add-Content versioninfo.h "#define VER_MINOR $verMinor"
    Add-Content versioninfo.h "#define VER_TIMESTAMP_DAYS    $days"
    Add-Content versioninfo.h "#define VER_TIMESTAMP_SECONDS $secs"
    Add-Content versioninfo.h "#define VER_FILEVERSION    ""$verMajor.$verMinor.$days.$secs"""
    Add-Content versioninfo.h "#define VER_PRODUCTVERSION ""$gitVersion"""
    type versioninfo.h

    $ENV:Path += ';c:\Program Files (x86)\Windows Kits\10\bin\10.0.17134.0\x86'
    $include  = 'c:\program files (x86)\Windows Kits\10\Include\10.0.17134.0\um;'
    $include += 'c:\program files (x86)\Windows Kits\10\Include\10.0.17134.0\shared'

    $cmd = 'rc -i"' + $include + '" versioninfo.rc'
    Write-Host ":>" + $cmd
    Invoke-Expression $cmd
  displayName: Calculate and Update VersionInfo

The pipeline first performs the GitVersion templated task to derive the necessary version information from the GitVersion configuration and commit history.

This has been updated recently to yield more information now than just the semantic version. Component values of that version information are now also set in environment variables which we can see the subsequent PowerShell script then uses to construct the content of the versioninfo.h header file.

This versioninfo.h file is first deleted (a development version is present in the repo so we can be certain that it will exist and need to be deleted as part of a build). Once the header file has been created as required, we emit it to the console to help identify and problems in the process when reviewing the build logs.

There is then a bit of minor house-keeping needed to prepare for the invocation of the Windows SDK resource compiler which produces the versioninfo.res file referenced in the project itself.

Once successfully compiled, the startup code of the duget executable is then able to extract it’s own version info. When duget commands are performed this startup code identifies duget in the console with output similar to:

Duget Package Manager 0.1.0-alpha.15
Build 0.1.7217.23479 (For more information visit https://duget.org)
(c)2019 Jolyon Direnko-Smith

Internally, the ProductVersion string provides the semantic, human meaningful version of the product whilst the PRODUCTVERSION identifies the Major.Minor version of the product without any Revision.Build info (both zeroed since they are not relevant to the product version).

The FILEVERSION and FileVersion string values both identify the detailed build version complete with date and time of the specific build that produced this exe.


How do you manage version info in your automated builds ? What do you think of this approach ?