Inno Setup custom data directory location

In my last post, I forgot to point you to Inno Setup; go get the Inno Setup QuickStart Pack to get going.
I showed how to copy a directory full of data, like tutorials or sample data, that might change depending on the customer. That means the files are not known when the installer is compiled.

Here's what it looked like:
Source: {src}\data\*; DestDir: C:\MyCompany\data;
   Flags: external recursesubdirs skipifsourcedoesntexist onlyifdoesntexist uninsneveruninstall;
   Permissions: users-modify

This time, I 'm going to show how to let the user choose where this directory is located, and whether to install the contents of the directory at all.

First, let's show an obvious choice:

Source: {src}\data\*; DestDir: {userdocs}\MyCompany\data; Flags: [as above....]

That new constant will put the data in a subdirectory of My Documents, for the user that installs the program. This might be fine for you, if each user of your program is going to install it themselves, and generate their own data. This is exactly what Microsoft had in mind with the My Documents folder.

However, it doesn't work for us. Often, an IT person will install the program, and several users on the computer with different logins will use it and need to access the same data. Our data is also big, so we don't want multiple copies made.

Here's what we actually use:

Source: {src}\data\*; DestDir: {code:GetDataDir}; Check: InstallSampleData; Flags: [as above...]

We use two 'code' pieces to make this work. First we as the user where to put the data, and the function GetDataDir retrieves that value. Second, we ask the user whether to install the data we provide, and InstallSampleData retrieves that answer. Here's what those two routines look like, in the [Code] section of our script:

function GetDataDir(Param: String): String;
  { Return the selected DataDir }
  //MsgBox('GetDataDir.', mbError, MB_OK);
  Result := DataDirPage.Values[0];

function InstallSampleData(): Boolean;
  { Return the value of the 'install' radiobutton }
  //MsgBox('InstallSampleData.', mbError, MB_OK);
  Result := SampleDataPage.Values[0];

Pretty simple - they are just retrieving the answers and returning them. The commented MsgBox calls can let you know when these routines are being called when you run your installer, for the curious.

But where are we getting these values? Custom wizard pages. We need some answers that don't really fit the normal flow of the installer, but can be inserted pretty easily. I followed the CodeDlg.iss example provided with Inno Setup pretty closely, so it's worth looking at that, too. I'm actually writing this blog post because there isn't an explanation to go along with some of the Inno Setup examples.

// global vars
  DataDirPage: TInputDirWizardPage;
  SampleDataPage: TInputOptionWizardPage;
  DataDirVal: String;

// custom wizard page setup, for data dir.
procedure InitializeWizard;
  { Taken from CodeDlg.iss example script }
  { Create custom pages to show during install }

  DataDirPage := CreateInputDirPage(wpSelectDir,
    '{#ShortAppName} Data Directory', '',
    'Please select a location for {#AppName} data. (We recommend the default.)',
    False, '');

  { Set default values, using settings that were stored last time if possible }
  if RegQueryStringValue(HKEY_LOCAL_MACHINE, 'SOFTWARE\MyCompanyProg',
     'DataDir', DataDirVal) then begin
    DataDirPage.Values[0] := DataDirVal;
  end else
    DataDirPage.Values[0] := 'C:\
// you might replace the previous with this, which is a per-user way
// to retrieve the previous value:
//   DataDirPage.Values[0] := GetPreviousData('DataDir', 'C:\MyCompany\data\');

  SampleDataPage := CreateInputOptionPage(DataDirPage.ID,
    'Install Sample Data', 'The sample scene is used in our tutorials and training.',
    'Should the sample scene be installed?   ( Recommended for new users, ~ XX GB )',
    True, False);
  SampleDataPage.Add('Do not install');

  SampleDataPage.Values[0] := True;

// This is needed only if you use
GetPreviousData above.
procedure RegisterPreviousData(PreviousDataKey: Integer);
  SetPreviousData(PreviousDataKey, 'DataDir', DataDirPage.Values[0]);

function DataDirExists(): Boolean;
  { Find out if data dir already exists }
  Result := DirExists(GetDataDir(''));

Whew! As you can see, it's starting to get long. Basically, I'm created two pre-formatted wizard pages, that let the use choose a directory, and choose between two (or more) options with a radio button. When they are display, their vals will get changed by the user. Then during the installation phase, those values are retrieved and used to create and copy the data directory. We are also using Inno Setup PreProcessor values for the application name in the dialog, so check that out in ISPPExample1.iss.

But wait, there's one more thing:

; If this directory already exists, it sets permissions on all files inside.
; this can take a LONG time (~5 min) so only install if it doesn't already exist.
Name: {code:GetDataDir}; Check: not DataDirExists; Flags: uninsneveruninstall; Permissions: users-modify

The directory won't get created if we don't put anything in it, so we need to include an entry in the Dirs section. And as the comment says, it's worth it to not touch it if it already exists, because setting permissions on a lot of files can take a LONG time, and the installer appears to hang while it's doing that.

That's it for a custom, shared data directory and the Inno Setup script to make it happen. Please let me know if you found this useful, or if it need fixes. Thanks!


Mako said…
Hey Aron...So I just found this post and it's (almost) a life-saver!! Thanks a whole lot!

Now I say "almost" cause it's not QUITE what I need...I'm trying to create an installer for a source port, and I'd like a page for the user to be able to copy the game files into the source port installation during the install.

If you'd be willing to help could you email me at "afuturepilotis(at)gmail(dot)com?

Thanks in advance!! (And even if you can't I might be able to figure it out from what you already have, so thanks anyway! ;)
Aron Helser said…
Hi Mako,
What do you mean by a 'source port'? Basically you want them to choose a directory, and then the installer goes and gets file from that directory?

I think that you should be allowed to use the value from the directory
control as a 'source' in Inno Setup, instead of a 'destination' like I do. You'd have to try it out....

Mako said…
Well it's a rebuild of the original game, but it requires a few of the original game files. It's actually two games (Descent 1 and Descent 2). In a perfect world this is how it would happen:

1. A page asking whether to install the source port for just Descent 1, just Descent 2, or both (I have this using the [components] section.)

2. A page asking where to install them to (already have this...default behavior.)

3. A Page having three checkbox options:
A. Copy game files, missions, and players.
B. Copy game files only
C. Don't copy any of them.

4. If they check A or B a page that has either one (If they only selected one of the components) or two (if they selected both) browse for folder dialogs, where they locate the original game installation folders.

5. If they selected C or after page 4, the regular install page (default behavior.)

Then I'd have in the Source: sections the constants created from the [code] section.

Can you help me do that? :)
Mako said…
OK, so I've got it pretty much figured out, but I've hit a snag...what's happening now, is that it's calling the GetDataDir function BEFORE the page is created, so I get a "could not call proc" error. Is there any way to manually tell it when to call the function?


(Also one other you know of a way to only add a browse dialog to a custom wizard page if the user selected a certain component?)
Aron Helser said…
> it's calling the GetDataDir
> function BEFORE the page is created
This is pretty weird. The code to install files should only get called after all the installer pages run, so that's not it. I'd bet that one page is referencing another page, so you might have to reverse the order they are declared in your script file.

As far as conditional UI, you should be able to put in inside an 'If' statement, using a condition set or changed by the previous page. I think CodeDlg.iss has an example?
Mako said…
Ok, I tried switching how they were declared and I still got the's my .iss file:

Could you look over it and see what I'm doing wrong? I'm getting the error on the "Descent" the line that begins "Result" :)

Thanks for all your help!!
Aron Helser said…
Hey Mako,
I think it might be because your 'Descent' function is declared like this:
function Descent(Param: String): String;
which means it expects a String argument, and it's not being supplied one with a [Files] entry like:
Source: "{code:Descent}\*.hog"; ...

Change to:
function Descent(): String;
my example code doesn't do it that way, though, so I'm not sure.
Mako said…
Sadly that didn't said it was declared wrong. (Something like that...The end result was it didn't compile)

I know when I uncommented the message box, it appears right after the select language screen...that's what was making me think it was calling the function too early.
Aron Helser said…
Mako, I don't see anything else obvious to help you. Please post a solution if you figure it out!
Mako said…
Alright after alot of looking and asking around I figured it out! :) I did that as well as several other things I's the finished .iss file for anyone who wants to see:

Thanks a ton Aron!!
Unknown said…
Hi Aron,

I am a new bee. As per my requirement i need get two different path for two folders. One is for IMPORTED folder and another one is for Workspaces folder.
I am able to create one folder not another one and after creating it i need to update the path of the folder in one text file. Please help.

Popular posts from this blog

OpenEmbedded Angstrom for Advantech PCM-9375

Inno Setup MSVC vcredist without bothering your users