Search code examples
arraysinno-setuppascalscript

Inno Setup: Single installer that reads names from an INI file and creates an installation for each name


I am using a Pascal Script function to read and detect how many names are in an array in a file. When it has determined the amount of names, I want a #for loop to iterate for that amount.

I can read and detect the number of names with Pascal Script. The problem is, I don't know how to after the name counting process, set the numberOfElements variable's value. It has to be set to the number of names just read with the Pascal Script function.

Here is some example code:

#define numberOfElements

#sub CreateSubInstallation
[Languages]
//code ommitted

[Files]
//code ommitted

[Run]
//code ommitted

#endsub
#for {i = 0; i < numberOfElements; i++} CreateSubInstallation

A different method of doing this would also be fine. I just want to read a number of names from a file and then make copies of the installation for the amount of names. Therefore each name has its' own installation. To elaborate even more, each name has it's own: directory,sub-directories and variables in files that will get the name "injected" into them.

Here is the format of the INI file:

[Customer]
customers={"customerName1","customerName2"}

Here is the code for reading and detecting the names with Pascal Script:

{ Get the customer names from file }
function GetCustomersFromFile(fileName : string): string;
var
  lines: TArrayOfString;
  amountOfLines,i: integer;
  tempResult: string;
begin
  tempResult := '';
  if LoadStringsFromFile(fileName, lines) then
  begin
    amountOfLines := GetArrayLength(amountOfLines);
    { Keep reading lines until the end of the file is reached }
    for i := 0 to amountOfLines - 1 do
    begin
      if (Pos('customers', lines[i]) = 1) then
        tempResult := lines[i];
    end;

    { if not found return -1 }
    if (tempResult = '') then
      { this result is for debugging and }
      { will have no impact on the array population process }
      tempResult := '-1';

    Result := tempResult;
  end;
end;

{ Count the number of elements present in the array in the file }
{ Done for tempArray initilization }
function CountNumberOfStringElements(line : string): integer;
const 
  chars = ['0'..'9', 'a'..'z', 'A'..'Z'];
var
  ignoreCommas: Boolean;
  numElements, numValidText: integer;
  i: integer;
begin
  ignoreCommas := false;
  numValidText := 0;
  numElements := 0;
  { Loop through text }
  for i := 0 to Length(line) - 1 do
  begin
    if (line[i] = '"') then
      ignoreCommas := (NOT ignoreCommas);

    if ((line[i]) IN chars AND (ignoreCommas)) then
      Inc(numValidText);

    if((line[i] = ',') AND (NOT ignoreCommas) )then
      Inc(numElements);
  end;

  if (numElements >= 1) then
    result := numElements + 1
  else if ((numElements = 0) AND (numValidText > 0)) then
    result := 1
  else if ((numElements = 0) AND (numValidText = 0)) then
    result := 0;
end;

This is essentially what I want the installer to do, just a very stripped down version.

[Setup]
AppName=My Program
AppVersion=1.5
WizardStyle=modern
DefaultDirName={autopf}\My Program
DefaultGroupName=My Program
UninstallDisplayIcon={app}\MyProg.exe
Compression=lzma2
SolidCompression=yes
OutputDir=userdocs:Inno Setup Examples Output
ArchitecturesAllowed=x64
ArchitecturesInstallIn64BitMode=x64

[Files]
Source: "MyProg-x64.exe"; DestDir: "{app}/customer1/"; DestName: "MyProg.exe"
Source: "MyProg.chm"; DestDir: "{app}/customer1/"
Source: "Readme.txt"; DestDir: "{app}/customer1/"; Flags: isreadme

Source: "MyProg-x64.exe"; DestDir: "{app}/customer2/"; DestName: "MyProg.exe"
Source: "MyProg.chm"; DestDir: "{app}/customer2/"
Source: "Readme.txt"; DestDir: "{app}/customer2/"; Flags: isreadme

[Icons]
Name: "{group}\My Program"; Filename: "{app}\MyProg.exe"

Please note the reason why it is structured like this is because they are services. Each service eventually gets more populated with customer related content. The entire installation process has to be done with an .exe and the removal process with a different but also singular .exe.


Solution

  • Your question is very unclear. But I'll try to give you some answer.

    If I understand correctly, you want to deploy the same set of files for each customer in your INI file. You cannot do that in Inno Setup using [Files] section, if you need to read the the INI file on run/install time (it is possible, if you read the INI file on compile time).

    If you need to clone the files on run/install time, all you can do is to install them to a temporary folder and then copy them over using Pascal Script.

    [Files]
    Source: "MyProg.exe"; DestDir: "{tmp}"
    
    [Code]
    procedure CurStepChanged(CurStep: TSetupStep);
    var
      I: Integer;
      SourcePath: string;
      TargetPath: string;
    begin
      if CurStep = ssPostInstall then
      begin
        for I := 0 to NumberOfCustomers - 1 then
        begin
          SourcePath := ExpandConstant('{tmp}\MyProg.exe');
          TargetPath := GetPathForCustomer(I) + '\MyProg.exe';
          if FileCopy(SourcePath, TargetPath, False) then
          begin
            Log(Format('Installed for customer %d', [I]));
          end
            else
          begin
            Log(Format('Failed to install for customer %d', [I]));
          end;
        end;
      end;
    end;
    

    (You have to replace NumberOfCustomers and GetPathForCustomer with your implementation)

    Though this way, you won't get any progress bar and you lose all built-in error handling of Inno Setup. You will also have to implement uninstallation in Pascal Script.


    It would definitely be better, if you read the INI file on a compile time. That means you will have to re-generate the installer with each change of the INI file. But that can be done with a command-line compiler by a single click.

    Though parsing your INI file with a preprocessor won't be easy.


    Another hackish solution is to generate a large number of identical entries in the [Files] section, which can then be dynamically associated with the customers on run/install time. It's not universal, as there will always be an upper limit. But if you know that you will never have a more than e.g. 100 customers, it is a viable option. And progress bar, error handling and uninstallation will work.


    I do not understand, what does [Languages] section have to do with the INI file, so I'm skipping that.


    Side note: Your GetCustomersFromFile and GetCustomersFromFile can be replaced with few lines of code with use of GetIniString and TStringList.CommaText. But that's for a separate question.