Category
In the latest video of Embarcadero's 30 for 30 series, Ian Barker gives a great overview of many of the popular installation builders to help you package and distribute your applications to virtually any platform. One of the most popular tools for us Delphi programmers targeting Windows is InnoSetup, a free and open-source tool by Jordan Russel. This solid program has been around for a very long time, it can handle nearly any Windows-based installation needs, AND is written in Delphi!
The documentation is good and it comes with several examples of how to build an installation from your compiled app so I won't regurgitate those here. Instead, I thought it might be useful to share some of the things I've done with it over the years for a variety of different scenarios as examples of how it can be used; you never know when a technique might be useful to someone (or even my future self!).
Setup
I use InnoSetup's preprocessor to declare several strings at the top of every InnoSetup script I write; these are used in later sections:
#define AppBaseName "MyNiftyApp" #define AppExeName AppBaseName + ".exe" #define SourcePath "V:\MyNiftyApp\src" #define SourceExe SourcePath + "\" + AppExeName #define AppVersion GetVersionNumbersString(SourceExe) #define AppVersionShort Copy(AppVersion, 1, RPos('.', AppVersion) - 1) #define AppName "David's Super Nifty App" #define AppPublisher "Cornelius Concepts, LLC" #define AppURL "http://CorneliusConcepts.com"
This calls InnoSetup's GetVersionNumbersString
function for the application I'm distributing, then shortens it with the Copy
and RPos
functions to cut off the build number for a nice display; notice how several of the lines build upon each other. Many of these are used in the [Setup]
section:
[Setup] AppName={#AppName} AppVersion={#AppVersionShort} AppVerName={#AppName} Vr. {#AppVersion} AppPublisher={#AppPublisher} AppPublisherURL={#AppURL} AppSupportURL={#AppURL}/contact_us OutputBaseFilename={#AppBaseName}Setup_{#AppVersionShort}
Smart Defaults
One of the jobs of a professional programmer is to ease the burden on the user as much as possible. This includes reducing the decision-making stress during installation. If you can build some smarts into your installer so that all the user has to do is confirm the actions about to take place, you've started off the user experience on the right foot.
I've built several plug-ins for Retail Pro, a complex retail management system that has over 30 years of history on the Windows platform and supports complex reporting, an XML-based import/export mechanism, and synchronization between remote stores. There are many moving parts and with this length of history, things have changed here and there over time, including the name of the company that publishes it.
To get a plug-in to be recognized with Retail Pro, it must be installed in a specific folder. Depending on how it's installed, it could either be running on a local machine accessing a network database or installed on a network and run remotely. Often times, store owners don't know where the software is physically installed, they simply know that they click on an icon and it starts up. So, I didn't want to leave it up to the user to select a destination for the plug-in, I had to figure it out myself. Fortunately, Retail Pro's installer stores its installed path in the registry; unfortunately, they are different depending on what version of Retail Pro is installed and the company that published that particular version AND whether it's running on a 32-bit or 64-bit machine (they're all 64-bit now but not 20 years ago!).
InnoSetup has great support for getting values out of the Registry and its scripting allowed me to find and return the right one. First, the [Setup]
section's DefaultDirName
was set to use the returned value of a custom function:
[Setup] ... DefaultDirName={code:FindRPro9Dir} ...
And here's the function it called:
[Code] function FindRPro9Dir(Param: string): string; var s: string; RetailProBaseKey: string; WinUninstallKey: string; RProGUID: string; begin RetailProBaseKey := 'SOFTWARE\Retail Pro\CE\RPro9\'; if IsWin64 then RetailProBaseKey := 'SOFTWARE\Wow6432Node\Retail Pro\CE\RPro9\'; RProGUID := '{10CEB7D3-68D5-42D5-A0FC-19DD97F1EED5}'; WinUninstallKey := 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\' + RProGUID; if IsWin64 then WinUninstallKey := 'SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\' + RProGUID; s := ''; if RegKeyExists(HKEY_LOCAL_MACHINE, RetailProBaseKey + '9.20\Inst1') then RegQueryStringValue(HKEY_LOCAL_MACHINE, RetailProBaseKey + '9.20\Inst1', 'INSTALLPATH', s) else if RegKeyExists(HKEY_LOCAL_MACHINE, RetailProBaseKey + '9.20 R2\Inst1') then RegQueryStringValue(HKEY_LOCAL_MACHINE, RetailProBaseKey + '9.20 R2\Inst1', 'INSTALLPATH', s) else if RegKeyExists(HKEY_LOCAL_MACHINE, RetailProBaseKey + '9.20 R3\Inst1') then RegQueryStringValue(HKEY_LOCAL_MACHINE, RetailProBaseKey + '9.20 R3\Inst1', 'INSTALLPATH', s) else if RegKeyExists(HKEY_LOCAL_MACHINE, RetailProBaseKey + '9.20 R4 \Inst1') then RegQueryStringValue(HKEY_LOCAL_MACHINE, RetailProBaseKey + '9.20 R4 \Inst1', 'INSTALLPATH', s) else if RegKeyExists(HKEY_LOCAL_MACHINE, WinUninstallKey) then RegQueryStringValue(HKEY_LOCAL_MACHINE, WinUninstallKey, 'InstallLocation', s); Result := s; end;
This calls InnoSetup's built-in IsWin64
function to help me know which registry keys to check. Then I systematically go through the various versions (that were available at the time this was written) and return the path to Retail Pro. Once that is found, I fill that in for the user so they don't have to go hunting around or find someone who knows.
Custom Messages
InnoSetup has a great set of labels to make its wizard-like installation go smoothly, look familiar, and guide you through the process. Sometimes, you might like to augment or even change some of the information displayed that might be pertinent to your software. For example, once the Retail Pro plug-ins (above) were installed, I wanted the user to configure them.
The [Messages]
section in InnoSetup scripts allow you to customize almost every label and screen in the setup process. There's a list of all the default messages and all you have to do is look through that list to find the identifers for the text you want to change and include that in your script. Here's the custom message I added that changed the final message after installing a Retail Pro plug-in:
[Messages] ClickFinish=Please select "Configure" from the File menu the fist time you run to confirm the Retail Pro path, Workstation Number and other settings.%n%nClick Finish to exit Setup.
Here, I've simply added one sentence before the default, "Click Finish" phrase that reminds the user to configure the plug-in.
In addition to changing messages during installation, you can also change messages during uninstallation. InnoSetup registers the installation with Windows and when you use Add/Remove Programs to uninstall your program, it calls the uninstaller which is built from the same script. The programs I distribute often create log files and I want to make sure the user knows the uninstall script does not delete those; sure, I could tell the script to delete files or even whole folders during uninstallation but I sometimes want to check the log files before doing that, so they are left there. Additionally, the Retail Pro plug-ins (mentioned above) can't always remove all files as installation registers DLLs which may be used by other plug-ins and will refuse to be uninstalled. In any case, I want the user to know that the program didn't completely clean up after itself. Therefore, another couple of lines are added to the [Messages]
section:
[Messages] #define NotUninstalled "NOTE: The configuration and any log files have NOT been removed." UninstalledAll=%1 was successfully removed from your computer.%n%n{#NotUninstalled} UninstalledMost=%1 uninstall complete.%n%nSome elements could not be removed. These can be removed manually.%n%n{#NotUninstalled}
There are two different message identifiers, UninstalledAll
, and UninstalledMost
. InnoSetup actually detects if it can't uninstall all files and shows the appropriate message. I want to customize both of them and add the comment that none of the log files were removed. Since that additional phrase is the same in both cases, I created a #define
to save myself some typing and make sure they're worded identically.
Conditionals add Supplemental Tools
Sometimes, customers encounter problems with your software that you cannot replicate and are difficult to debug. There are tools that can provide extra logging or send system information via email or other methods that you can include with your software but often these make the size of the distribution significantly larger and you wouldn't normally want to distribute them; but if a customer has a problem and you need to resort to including extra support, you can add this to your InnoSetup script conditionally so you can toggle it off later.
One of the tools I've used is CodeSite Logging. This can provide a huge amount of additional insight into your code running remotely, it can save to log files or even send messages directly to an IP Address giving you a real-time view of your code running remotely. Supporting CodeSite requires installing a small program which registers a "listener" on the user's computer; additionally, if you want the end user to see the messages generated, you can include the CodeSite Viewer. Adding these in requires several additions to the script; I don't want to make a separate script for this but want to enable or disable this extra debugging as I need throughout the life of the product. Therefore, the first thing I'm going to do is declare a constant at the top of the script that I can check later on:
#define CSDebugging
By defining the "CSDebugging" identifier in the script, I can check to see if it's defined later on:
#ifdef CSDebugging ... #endif
Now, in the [Files]
section, I added the additional files I need to distribute to enable CodeSite logging remotely:
[Files] ... #ifdef CSDebugging Source: "C:\Program Files (x86)\Raize\CS4\Tools\CS4_Tools.exe"; DestDir: "{app}"; Flags: deleteafterinstall; #endif
This tool needs to be run once at the end of the installation in order to register the CodeSite logging mechanism:
[Run] #ifdef CSDebugging Filename: "{app}\CS4_Tools.exe"; Parameters: "/s /m=CodeSiteToolsSetup.txt"; StatusMsg: "Installing CodeSite logging tool..."; Description: "CodeSite debugging log viewer"; #endif
Prevent Overwriting
One of the applications I wrote included a customizable dictionary and enabled spell-check. The customer had a lot of unique clients in their database and over time, built up a list in their user dictionary so the spell-check wouldn't flag them. I quickly learned to add a flag to the installation of the dictionary files so that if the dictionary files already existed, they would NOT be overwritten!
[Files] Source: "AddictDictionaries\american.adm"; DestDir: "{userappdata}\{#AppName}"; Flags: onlyifdoesntexist Source: "AddictDictionaries\autocorrect.adu"; DestDir: "{userappdata}\{#AppName}"; Flags: onlyifdoesntexist
Summary
There's so much more that InnoSetup can do than what I've shown. It can install fonts, register COM libraries, display a custom license agreement that must be accepted, prompt for and validate a serial number, add custom images to brand the installation steps, support multi-lingual messages, create desktop shortcuts, include other scripts, and more! Read through the documentation, search for scripts on Github, and ask for help on forums. When deploying to Windows, you likely don't need anything else!
Add new comment