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