Inno Setup Installer
Using Inno Setup to Create a Versioned Installer

An important piece of any application is an easy-to-use installer. If tool is difficult to setup, it's unlikely to get used. For Windows distributions, AgileTrack uses Inno Setup to generate custom setup executables. Scripts were creating to build both full installers and update installers. It is assumed that the reader has basic familiarity with Inno Setup and its setup scripts.

The installation script is designed to require little maintenance. It is driven by a couple data files which contain the application major, minor, and build version values. Incidentally, those files are used by other aspects of the build process, and it is important that the application version is consistent throughout, and easy to maintain.

The version values are obtained from the data files and used to dynamically generate the application name as displayed by the installer, store the installed version of the application in the registry, and compare the version of an existing installation with an update installation being performed.

The most important thing to know is how to access a data file in the installer that is not intended to be installed on the user's system. First of all, the data files must be included in the [Files] section of the .iss file as shown:

[Files]
; Data files are destined for the temp directory and are not 
; copied as part of the install process
Source: major.dat; DestDir: {tmp}; Flags: dontcopy
Source: minor.dat; DestDir: {tmp}; Flags: dontcopy
Source: build.dat; DestDir: {tmp}; Flags: dontcopy
           

Notice that the files are meant for the system temporary directory, and the dontcopy flag means they will not be copied as part of the installation. Including the data files in that manner ensures that they will be included in the executable generated by the install generator.

Reading and using the values from those files within the installer script involves some custom scripting in the [Code] section of the installer script. Essentially, each file needs to be extracted, and then read from its extracted location into a variable for use:

[Code]
; Each data file contains a single value and can be loaded after extracted.
; The filename and DestDir from the [Files] section must match the names
; and locations used here
function GetAppMajorVersion(param: String): String;
	var
		AppVersion: String;
	begin
		ExtractTemporaryFile('major.dat');
		LoadStringFromFile(ExpandConstant('{tmp}\major.dat'), AppVersion);
		Result := AppVersion;
	end;

function GetAppMinorVersion(param: String): String;
	var
		AppMinorVersion: String;
	begin
		ExtractTemporaryFile('minor.dat');
		LoadStringFromFile(ExpandConstant('{tmp}\minor.dat'), AppMinorVersion);
		Result := AppMinorVersion;
	end;
	
function GetAppCurrentVersion(param: String): String;
	var
		BuildVersion: String;
	begin
		ExtractTemporaryFile('build.dat');
		LoadStringFromFile(ExpandConstant('{tmp}\build.dat'), BuildVersion);
		Result := BuildVersion;
	end;
           

Notice that each of the functions provides a single String parameter which is not actually used by the function. This is done with purpose and will be seen below. At this point, we are able to store custom version values in a file, and load them within the installer script.

AgileTrack displays the version as part of the application name in the installer script. It also writes the version and build number to the registry so that update installers can verify the existing installed version. Doing so is as simple as including {code:GetAppMajorVersion|''} in the sections where the value is to be used. The {code:Function|Param} feature allows functions to be executed inline as part of the values in section definitions. Notice the empty String parameter which our function signatures needed to handle.

[Setup]
; App version name is dynamically derived from the included version data files
AppVerName=AgileTrack {code:GetAppMajorVersion|''}{code:GetAppMinorVersion|''} build {code:GetAppCurrentVersion|''}
           
[Registry]
; Version values are stored in the registry as part of the installation process
Root: HKLM; Subkey: Software\AgileTrack; ValueType: string; ValueName: Version; ValueData: {code:GetAppMajorVersion|''}; Flags: uninsdeletekey
Root: HKLM; Subkey: Software\AgileTrack; ValueType: string; ValueName: CurrentVersion; ValueData: {code:GetAppCurrentVersion|''}; Flags: uninsdeletekey
           

Having been able to utilize external files which contain version information, we've been able to customize the installer by simply modifying a few data files. When it comes time to update an existing installation, we want to give the user some feedback as to whether or not the upate is necessary or progressive. We also want to make sure the installation is automatically done to the same directory as the previous installation. To do this, we need to be able to read several values out of the registry that had been placed there by the previous installation.

; Inno Setup creates Uninstall registry keys automatically.  We know out AppID, and
; how Inno Setup names its keys, so we can find the existing installation path
function GetPathInstalled(AppID: String): String;
	var
		PrevPath: String;
	begin
		PrevPath := '';
		if not RegQueryStringValue(HKLM, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\'+AppID+'_is1', 'Inno Setup: App Path', PrevPath) then begin
			RegQueryStringValue(HKCU, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\'+AppID+'_is1', 'Inno Setup: App Path', PrevPath);
		end;
		Result := PrevPath;
	end;

; We put the installed version in the registy in the original installation
function GetInstalledVersion(): String;
	var
		InstalledVersion: String;
	begin
		InstalledVersion := '';
		RegQueryStringValue(HKLM, 'Software\AgileTrack', 'Version', InstalledVersion);
		Result := InstalledVersion;
	end;

; We put the current build version in the registry in the original installation
function GetInstalledCurrentVersion(): String;
	var
		InstalledCurrentVersion: String;
	begin
		InstalledCurrentVersion := '';
		RegQueryStringValue(HKLM, 'Software\AgileTrack', 'CurrentVersion', InstalledCurrentVersion);
		Result := InstalledCurrentVersion;
	end;
           

Having loaded those important values, all we need to do now is use them when initializing the installer to make some decisions about whether or not the installation should be performed.

function InitializeSetup(): Boolean;
	var
		Response: Integer;
		PrevDir: String;
		InstalledVersion: String;
		InstalledCurrentVersion: String;
		VersionError: String;
	begin
		Result := true;

		// read the installtion folder
		PrevDir := GetPathInstalled(getAppID(''));

		if length(Prevdir) > 0 then begin
			// I found the folder so it's an upgrade
			InstalledVersion := GetInstalledVersion();
			// compare versions
			if InstalledVersion = GetAppMajorVersion('') then begin
				InstalledCurrentVersion := GetInstalledCurrentVersion();
				if (InstalledCurrentVersion < GetAppCurrentVersion('')) then begin
					Result := True;
				end else if (InstalledCurrentVersion = GetAppCurrentVersion('')) then begin
					Response := MsgBox(
						'It appears that the existing AgileTrack installation is already current.' + #13#13 +
						'Do you want to continue with the update installation?', mbError, MB_YESNO
					);
					Result := (Response = IDYES);
				end else begin
					Response := MsgBox(
						'It appears that the existing AgileTrack installation newer than this update.' + #13#13 +
						'The existing installation is build '+ InstalledCurrentVersion +'.  This update will change the installation to build '+ GetAppCurrentVersion('') + #13#13 +
						'Do you want to continue with the update installation?', mbError, MB_YESNO
					);
					Result := (Response = IDYES);
				end;
			end else begin
				if length(InstalledVersion) = 0 then begin
					VersionError := 'Setup was unable to determine the version of the existing AgileTrack installation.';
				end else begin
					VersionError := 'Setup has detected an installation of AgileTrack ' + InstalledVersion + '.';
				end;
				MsgBox(
					VersionError + #13#13 +
					'This update installer requires AgileTrack ' + GetAppMajorVersion('') +' to ' +
					'already be installed.' + #13 + 'Please install AgileTrack ' + GetAppMajorVersion('') +' before running this update.' + #13#13 +
					'Setup/Upgrade aborted.', mbError, MB_OK
				);
				Result := false;
			end;
		end else begin
			MsgBox(
				'This update installer requires an existing installation of AgileTrack ' + GetAppMajorVersion('') +' to ' +
				'already be installed.' + #13 + 'Please install AgileTrack ' + GetAppMajorVersion('') +' before running this update.' + #13#13 +
				'Setup/Upgrade aborted.', mbError, MB_OK
			);
			Result := false;
		end;
  end;
           

The InitializeSetup function handles several possible conditions and the user is notified of them when they occur--such as a previous installation not found, or existing installation being newer than the update, etc.

Finally, since an update installation is being performed, the user does not need to be prompted about selecting an install destination, Start menu items, etc., so we skip the appropriate wizard pages.

function ShouldSkipPage(PageID: Integer): Boolean;
	begin
		// skip selectdir if It's an upgrade
		if (PageID = wpSelectDir) then begin
			Result := true;
		end else if (PageID = wpSelectProgramGroup) then begin
			Result := true;
		end else if (PageID = wpSelectTasks) then begin
			Result := true;
		end else begin
			Result := false;
		end;
	end;
           

That's how AgileTrack Windows installers are generated. Each time a new release is built, the appropriate version data files are updated and the installer automatically utilizes them meaning the installer scripts themselves don't require any maintenance.

Here are the complete installer and update installer scripts:

Download: agiletrack.iss
Download: agiletrack-upgrade.iss