Deploying applications with Inno Setup

Well, this is it, the long-procrastinated post. I’ve been meaning to write this for a while now; a common problem that I’m sure most TDs (and even seasoned programmers!) face when all is said and done, and it’s time to actually let other human beings who aren’t knee-deep in your code use the amazing new editor/tool/program/framework you’ve been working on for god knows how long.

If you’ve been conscientious about documentation and continuous integration up to this point, you probably already have some sort of build system and process in mind. (If not, you really should!) But chances are if you’re reading this that you’ve been asked by an end-user to make a “one-click installer”, even after you’ve painstakingly taught them about Git-Flow and what a “fork” is. So what can you do?

The Options

If you’re capable of reading, you’ve probably guessed that the major point of this post is to talk about Inno Setup, a tool specifically designed for the purpose of creating deployable installers. However, before diving in, I’d like to give a brief overview of other options and why I chose not to use them for deployment of my own tools.

  • NSIS: Short for Nullsoft Scriptable Install System, this is frequently trumpted as a superior, more flexible alternative to Inno Setup. Like Inno Setup, it enjoys popular community support, has an extensive list of available public plugins to choose from, and also is based off a scripting language. (It also offers an Eclipse plugin, if you actually enjoy Eclipse as an IDE like some crazy people I know.) In fact, the game I currently work on uses NSIS to build its installers!
    • However, I feel that it takes far too long to get anything done with NSIS’s scripting language, and with Pascal scripting support in Inno Setup these days, anything complicated I want to do in it is also perfectly viable, as I will demonstrate in the later part of this post.
    • The fact that I can perform a ton of actions through the GUI of Inno Setup, and still enjoy the benefit of a command line for me makes it a better tool for deployment, especially since deployment really should be the least painful part of the process of making an application.
  • Deploying via Source Control Management (Git, SVN, Perforce etc.) Of course, I could write batch scripts that artists could run to automatically pull from a repository and a specific branch (which is what I did on Never Alone!).
    • A fellow TD has also described a setup in which he performed the git-pull, but all the artists had to do was do a reload() in Maya and their local repository would grab the updated files just fine. Unreal 4‘s developer installation procedure is pretty much a good example of this at work, too.
    • This makes sense for a studio that is kept under strict control, and has the artists’ workstation working environment all managed by the technical directors.
    • However, my solution has to account for outsourcers, and also the fact that I am deploying additional standalone applications with the tools that I provide. I have no control over what artists might/might not have on their computers, even Git, and so I need a solution that allows me to essentially install dependencies as needed. And in the case of UE4, they too provide two options; if you’re technically-inclined, you are free to work from the Git repository with the aid of some batch scripts to help setup dependencies, but for the artists, there exists a really nice launcher and installer for you to manage your installation of the editor.
  • PyInstaller: PyInstaller is certainly a very useful tool, but it is oriented towards packaging an entry point module into a runtime executable/app package, and not really well suited for handling just issues of “place these files here and these files there but set these environment variables permanently”. I plan to write another follow-up regarding this, but in my mind, PyInstaller is only part of the solution for deployment.
  • Python wheels: If you’ve worked with Python and pip, you probably already know what these are. But just in case you don’t, this is essentially the standard method of distribution for Python packages and libraries, and are what actually gets downloaded when you execute a command like pip install PyInstaller.
    • The reason why these are (to me) unsuitable for distribution of art tools is, well, how on earth are you going to use pip to install your packages to the Maya scripts folder without setting up, essentially, hacky batch scripts of some sort, which at that point would be the same as option #2?

So what can I do in Inno Setup?

Rather than being yet another tutorial on Inno Setup’s scripting or just dumping the code from one of my installers, I thought I’d instead use this post to just illustrate a few problems I faced when deciding to make a deployment solution, and what I did to resolve them.

How do I increment my installer automatically when building a new version?

This is a little tricky, and I admit that even my solution isn’t perfect. What I do is create a version.ini file in the same directory as my *.iss Inno Setup script, and include the following code in the header:

Read previous build number from INI config. If no build number exists, set to 0
#define MyAppVersion Int(ReadIni(SourcePath+"\\version.ini", "Info", "Build", "0"))

; Increment build number and write to INI
#expr MyAppVersion = MyAppVersion + 1
#expr WriteIni(SourcePath+"\\version.ini", "Info", "Build", MyAppVersion)

And in your version.ini, you would have the following:

[Info]
Build=1039

After this, go ahead and reference MyAppVersion in areas that you need the build number to appear. For example, to have the filename include the build number, place the following in the [Setup] directive:

OutputBaseFilename=setup_r{#MyAppVersion}

Now, every time your installer builds, it should read the number specified in the INI file and automatically increment the build number by 1.

How do I set environment variables during installation?

In the [Registry] block of your installation script, use the following code:

Root: "HKLM64"; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; ValueType: string; ValueName: "MY_ENV_VARIABLE"; ValueData: "{app}"; Flags: uninsdeletevalue

The environment variable MY_ENV_VARIABLE will be set to the application’s root directory upon installation.

How do I install Python for users who may not have it?

Ah, the big one. As TDs working in the world of digital content creation, Python is everywhere, and we can’t escape having to work with it and figure out ways of deploying it to our users (Not that Python is a bad thing! End-users, on the other hand…)

To start with, we’re going to have to write some Pascal to check if the registry has Python already installed. Thankfully, on Windows, by default, Python writes an environment variable to the registry when it installs itself through the standard MSI installer.

Thus, in the [Code] directive of the installer:

// This function checks the registry for an existing Python 2.7.x installation
function IsPythonInstalled: boolean;
begin
    Result := RegKeyExists(HKLM64, 'SOFTWARE\Python\PythonCore\2.7' );
end;

We then write another function that checks if Python exists on the system, and fails the installation procedure if it does not exist:

// This function returns True if Python 2.7 registry key is detected on the system
function DependenciesSetup(Param: String): Boolean;

var
  check : Boolean;

begin

    if WizardSilent() then
    begin
      Result := True;
      Exit;
    end;

    check := IsPythonInstalled;
    if not check then
    begin
        MsgBox('Could not install' + Param + 'because Python 2.7.x was not detected on the system!', mbInformation, MB_OK);
        Result := False;
    end;

    Result := True;
end;

After this, we write a simple function to prompt the user to automatically install Python if a False result is returned:

// This function checks if Python is installed and brings up user confirmation to automatically install it
function PythonSetup(): boolean;

var
  installPythonResult : Boolean;
  check : Boolean;

begin
    // Check if installer is being run in silent mode and skip all dialogs
    if WizardSilent() then
    begin
      Result := True;
      Exit;

    end;

    check := IsPythonInstalled;
    if not check then
        begin
          installPythonResult := MsgBox('The toolkit requires Python 2.7.6 to be installed! Do you want to have it automatically installed for you?', mbConfirmation, MB_YESNO) = IDYES;

          if not installPythonResult then
            begin
              MsgBox('Python 2.7.6 will not be installed! Remember to install it later manually on your own!', mbInformation, MB_OK);
              Result := False;
            end;
        end;

    Result := True;

end;

Now, in the Run directive of the installer, we give the following command:

; Install Python 2.7.6
Filename: "msiexec"; Parameters: "/i ""{app}\dependencies\python-2.7.6.amd64.msi"" /qb! ALLUSERS=1 ADDLOCAL=ALL"; Flags: 64bit; Description: "Install Python 2.7.6 AMD64"; MinVersion: 0,6.0; Components: connect; Check: PythonSetup

And of course, including the actual MSI installer file in the /dependencies folder is a must, which obviously increases deployment size, but it’s better than, I think, having several users all try to connect to the Python dist servers at once.

Finally, we also would like to append the Python install directory to the system PATH, so let’s write a function to do that in the [Code] directive, and run it from the [Registry] directive:

// This function will return True if the Param already exists in the system PATH
function NeedsAddPath(Param: String): Boolean;
var
  OrigPath: String;

begin
    if not RegQueryStringValue(HKLM64, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'PATH', OrigPath) then 
    begin
        Result := True;
        exit;
    end;

    // look for the path with leading and trailing semicolon; Pos() returns 0 if not found
    Result := Pos(';' + Param + ';', ';' + OrigPath + ';') = 0;

end;
; Append python to PATH if does not already exist
Root: "HKLM64"; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; ValueType: expandsz; ValueName: "PATH"; ValueData: "{olddata};{sd}\Python27\;{sd}\Python27\Scripts\"; Components: connect; Check: NeedsAddPath('{sd}\Python27')

This is pretty good: however, we’re not quite done yet: See below for more information on installing Python dependencies next and the problems associated with that as well.

How do I install Python dependencies for users?

After you’ve ensured that your users have an installation of Python, the next step is ensuring that they have the dependencies you’re looking for (not droids).

If you’ve been following along with the answers so far, this is actually fairly trivial.

First, the path of least resistance to getting packages set up is through using pip, so let’s install that first in the [Run] directive:

; Install pip and setuptools
Filename: "{sd}\Python27\python.exe"; Parameters: """{app}\dependencies\get-pip.py"""; Description: "Install pip"; Components: connect; Check: DependenciesSetup('pip')
Filename: "{sd}\Python27\python.exe"; Parameters: """{app}\dependencies\ez_setup.py"""; Description: "Install setuptools"; Components: connect; Check: DependenciesSetup('setuptools')

Now that we have pip installed, we can easily install dependencies as required. Whether you choose to do this through manually specifying them here as I have, or through a requirements.txt file is up to you.

; Install Python dependencies
Filename: "cmd.exe"; Parameters: "/C ""{sd}\Python27\Scripts\pip.exe install boto==2.28.0"""; Description: "Install boto 2.28.0"; Components: connect; Check: DependenciesSetup('boto')
Filename: "cmd.exe"; Parameters: "/C ""{sd}\Python27\Scripts\pip.exe install PySide==1.2.2"""; Description: "Install PySide"; Components: connect; Check: DependenciesSetup('PySide')
Filename: "cmd.exe"; Parameters: "/C ""{sd}\Python27\Scripts\pip.exe install riffle"""; Description: "Install Riffle"; Components: connect; Check: DependenciesSetup('riffle')

But one more thing: what if your user isn’t connected to the Internet when they run the application? Would you have to bundle every python wheel that you need and use pip to install from those files then?

Read on for information on how to check for a working Internet connection!

How do I check for a working internet connection?

This is where things get a little complicated. Essentially, the best way I’ve found to do this is to create a COM object that will try to open a HTTP request to a pre-defined URL; if it fails, or no response is received (length of response is 0) then the user is determined to have no connection to the Internet.

In code, this looks like:

// Check for working internet connection
function CheckInternetConnection(Param: String) : Boolean;

var
  WinHttpReq : Variant;

begin

  try
    // Create COM object to handle net connection attempt
    WinHttpReq := CreateOleObject('WinHttp.WinHttpRequest.5.1');
    WinHttpReq.Open('GET', Param, false);
    WinHttpReq.Send();

  except
    MsgBox('Could not connect to: ' + Param + '!' + #13#10 + 'Ensure that this computer has a working internet connection!', mbError, MB_OK);

  end;

  // Check for timeout
  if WinHttpReq.Status <> 200 then begin
    MsgBox('Could not connect to' + Param + '! Connection timed out!', mbError, MB_OK);
    Result := False;
  end;

  if Length(WinHttpReq.ResponseText) > 0 then begin
    Result := True;
  end;

end;

In your InitializeSetup() function (This method is executed before the setup installer is initialized), you would then include the following:

  // Connect to Python package dist server
  checkNetCxn := CheckInternetConnection('https://pypi.python.org/pypi');
  if not checkNetCxn then
  begin
    MsgBox('Please ensure that this computer has a working internet connection and try again!', mbError, MB_OK)
    Result := False;
    Exit;
  end;

How do I detect if a previous version was installed?

Of course, every application is never finished: there’s always updates, bugfixes and hotfixes to come with every deployment. The question is how to make this process as painless and idiot-proof as possible. For me, the answer is simply the Autodesk method: just run the new installer.

Firstly, something to note: when you install something through Inno Setup, a registry key with the UUID of your application is written (That’s why those keys are important!). Thus, we can write a method to check if it exists:

// This method checks for presence of uninstaller entries in the registry and returns the path to the uninstaller executable.
function GetUninstallString: String;

var
  uninstallerPath: String;
  uninstallerString: String;

begin
  Result := '';

  // Get the uninstallerPath from the registry
  uninstallerPath := ExpandConstant('SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{{6525583A-1BF8-43C9-AF77-59A4813D207E}_is1');

  uninstallerString := '';

  // Check if uninstaller entries in registry have values in them
  if not RegQueryStringValue(HKLM64, uninstallerPath, 'UninstallString', uninstallerString) then
    RegQueryStringValue(HKCU, uninstallerPath, 'UninstallString', uninstallerString);

  // Return path of uninstaller to run  
  Result := uninstallerString;

end;

Now that we know the result of the key’s existence, we can write a simple check to return a boolean instead, which is cleaner:

// This method checks if a previous version has been installed
function PreviousInstallationExists : Boolean;
begin
  // Check if not equal '<>' to empty string and return result
  Result := (GetUninstallString() <> '');
end;

And finally, in the InitializeSetup() method, we can write the following check:

// Now check if previous version was installed
  previouslyInstalledCheck := PreviousInstallationExists;
  if previouslyInstalledCheck then
  begin
    uninstallChoiceResult := MsgBox('A previous installation was detected. Do you want to uninstall the previous version first? (Recommended)', mbInformation, MB_YESNO) = IDYES; 

    // If user chooses, uninstall the previous version and wait until it has finished before allowing installation to proceed
    if uninstallChoiceResult then
    begin
      uninstallPath := RemoveQuotes(GetUninstallString());
      Exec(ExpandConstant(uninstallPath), '', '', SW_SHOW, ewWaitUntilTerminated, iResultCode);

      Result := True;
    end

    else
    begin
      Result := True;
      Exit;
    end;
  end

  else
    Result := True;

Now, whenever the installation is run, it will check if a prior version exists, prompt the user to uninstall it, and then continue with the installation.

Well, there you go! There’s a lot more fun stuff that you can do in Inno Setup, but I think that covers most of the issues that people run into on a daily basis.

For reference, a full installer script with the examples shown here is available on pastebin as well.

I hope this has been helpful to anyone going through the same pain of deployment that I have, and if you have any suggestions for re-factoring or questions for other issues you have with Inno Setup, feel free to drop a message and I’d be happy to curse along with you help brainstorm a solution!

(Also, this is probably the longest post I’ve ever written so far. Must be all my practice writing tools documentation as of late.)

2 comments on “Deploying applications with Inno SetupAdd yours →

  1. Thanks for spending the time to write this. It really helps to have the examples as well as knowing the pros and cons over the different setups.

Leave a Reply