Browse Source

Add support for dowload+extractarchives in a simple and clean way 👍

For such entries the archive is downloaded to {tmp}\_isetup\<randomdir>\<destname> using a TDownloadWizardPage, as the first step of PrepareToInstall. Supports verification.

On success the entries' SourceFilename is updated to the temp file, the download flag is removed and also DestName and verification. Áfter that the rest (PreviousInstallCompleted, RegisterResourcesWithRestartManager, and installation) works normally and required no changes.

On error the problem is displayed by the ready page. Also didn't require changes, except for an extract on BaseName display.

Todo:
-Rename CodeDownloadFiles.iss since there's no [Code] in it anymore.
-Offer Abort/Retry when a download fails? Or even Ignore somehow?
-Let the user choose if it should show BaseNames or URLs while downloading with a new directive? Both for archives and files.
-Document
Martijn Laan 3 months ago
parent
commit
2d0ec7b9e5

+ 15 - 57
Examples/CodeDownloadFiles.iss

@@ -3,6 +3,10 @@
 ; This script shows how the [Files] section can be used to download files and
 ; This script shows how the [Files] section can be used to download files and
 ; archives while showing the download and extraction progress to the user.
 ; archives while showing the download and extraction progress to the user.
 ;
 ;
+; Archives will be downloaded to temporary files at the start of the Preparing
+; To Install step. Other files will be downloaded directly to their destination
+; during the actual installation. 
+;
 ; To verify the downloaded files, this script shows two methods:
 ; To verify the downloaded files, this script shows two methods:
 ; -For innosetup-latest.exe and MyProg-ExtraReadmes.7z: using Inno Setup
 ; -For innosetup-latest.exe and MyProg-ExtraReadmes.7z: using Inno Setup
 ;  Signature Tool, the [ISSigKeys] section, and the AddWithISSigVerify support
 ;  Signature Tool, the [ISSigKeys] section, and the AddWithISSigVerify support
@@ -20,9 +24,9 @@ DefaultDirName={autopf}\My Program
 DefaultGroupName=My Program
 DefaultGroupName=My Program
 UninstallDisplayIcon={app}\MyProg.exe
 UninstallDisplayIcon={app}\MyProg.exe
 OutputDir=userdocs:Inno Setup Examples Output
 OutputDir=userdocs:Inno Setup Examples Output
-;Use "ArchiveExtraction=enhanced" if your archive has a password
-;Use "ArchiveExtraction=full" if your archive is not a .7z file but for example a .zip file
-ArchiveExtraction=enhanced/nopassword
+ArchiveExtraction=full
+;Use "ArchiveExtraction=enhanced" if all your archives are .7z files
+;Use "ArchiveExtraction=enhanced/nopassword" if all your archives are not password-protected
 
 
 [ISSigKeys]
 [ISSigKeys]
 Name: mykey; RuntimeID: def02; \
 Name: mykey; RuntimeID: def02; \
@@ -35,64 +39,18 @@ Name: mykey; RuntimeID: def02; \
 Source: "MyProg.exe"; DestDir: "{app}"
 Source: "MyProg.exe"; DestDir: "{app}"
 Source: "MyProg.chm"; DestDir: "{app}"
 Source: "MyProg.chm"; DestDir: "{app}"
 Source: "Readme.txt"; DestDir: "{app}"; Flags: isreadme
 Source: "Readme.txt"; DestDir: "{app}"; Flags: isreadme
-; These files will be downloaded using [Files] only
+; These files will be downloaded and verified
 Source: "https://jrsoftware.org/download.php/is.exe?dontcount=1"; DestName: "innosetup-latest.exe"; DestDir: "{app}"; \
 Source: "https://jrsoftware.org/download.php/is.exe?dontcount=1"; DestName: "innosetup-latest.exe"; DestDir: "{app}"; \
   ExternalSize: 7_000_000; Flags: external download ignoreversion issigverify
   ExternalSize: 7_000_000; Flags: external download ignoreversion issigverify
 Source: "https://jrsoftware.org/download.php/iscrypt.dll?dontcount=1"; DestName: "ISCrypt.dll"; DestDir: "{app}"; \
 Source: "https://jrsoftware.org/download.php/iscrypt.dll?dontcount=1"; DestName: "ISCrypt.dll"; DestDir: "{app}"; \
   Hash: "2f6294f9aa09f59a574b5dcd33be54e16b39377984f3d5658cda44950fa0f8fc"; \
   Hash: "2f6294f9aa09f59a574b5dcd33be54e16b39377984f3d5658cda44950fa0f8fc"; \
   ExternalSize: 2560; Flags: external download ignoreversion
   ExternalSize: 2560; Flags: external download ignoreversion
-; These files will be downloaded by [Code]. If you include flag issigverify here the file will be verified
-; a second time while copying. Verification while copying is efficient, except for archives.
-Source: "{tmp}\MyProg-ExtraReadmes.7z"; DestDir: "{app}"; Flags: external extractarchive recursesubdirs ignoreversion
+; This file will be downloaded, verified and extracted
+Source: "https://jrsoftware.org/download.php/myprog-extrareadmes.7z"; DestName: "MyProg.ExtraReadmes.7z"; DestDir: "{app}"; \
+  ExternalSize: 269; Flags: external download extractarchive recursesubdirs ignoreversion issigverify
+; This file will be downloaded and extracted without verificaton
+Source: "https://github.com/jrsoftware/issrc/archive/refs/heads/main.zip"; DestName: "issrc-main.zip"; DestDir: "{app}"; \
+  ExternalSize: 15_000_000; Flags: external download extractarchive recursesubdirs ignoreversion
 
 
 [Icons]
 [Icons]
-Name: "{group}\My Program"; Filename: "{app}\MyProg.exe"
-
-[Code]
-var
-  DownloadPage: TDownloadWizardPage;
-  AllowedKeysRuntimeIDs: TStringList;
-
-procedure InitializeWizard;
-begin
-  DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), nil);
-  DownloadPage.ShowBaseNameInsteadOfUrl := True;
-  
-  // To allow all keys you can also just pass nil instead of this list to AddWithISSigVerify 
-  AllowedKeysRuntimeIDs := TStringList.Create;
-  AllowedKeysRuntimeIDs.Add('def02');
-end;
-
-procedure DeinitializeSetup;
-begin
-  if AllowedKeysRuntimeIDs <> nil then
-    AllowedKeysRuntimeIDs.Free;
-end;
-
-function NextButtonClick(CurPageID: Integer): Boolean;
-begin
-  if CurPageID = wpReady then begin
-    DownloadPage.Clear;
-    // Use AddEx or AddExWithISSigVerify to specify a username and password
-    DownloadPage.AddWithISSigVerify(
-      'https://jrsoftware.org/download.php/myprog-extrareadmes.7z', '',
-      'MyProg-ExtraReadmes.7z', AllowedKeysRuntimeIDs);
-    DownloadPage.Show;
-    try
-      try
-        // Downloads the files to {tmp}
-        DownloadPage.Download;
-        Result := True;
-      except
-        if DownloadPage.AbortedByUser then
-          Log('Aborted by user.')
-        else
-          SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbCriticalError, MB_OK, IDOK);
-        Result := False;
-      end;
-    finally
-      DownloadPage.Hide;
-    end;
-  end else
-    Result := True;
-end;
+Name: "{group}\My Program"; Filename: "{app}\MyProg.exe"

+ 1 - 1
Projects/Src/Compiler.Messages.pas

@@ -290,7 +290,7 @@ const
     '"dontcopy" flag is used';
     '"dontcopy" flag is used';
   SCompilerFilesWildcardNotMatched = 'No files found matching "%s"';
   SCompilerFilesWildcardNotMatched = 'No files found matching "%s"';
   SCompilerFilesDestNameCantBeSpecified = 'Parameter "DestName" cannot be specified if ' +
   SCompilerFilesDestNameCantBeSpecified = 'Parameter "DestName" cannot be specified if ' +
-    'the "Source" parameter contains wildcards or flag "extractarchive" is used';
+    'the "Source" parameter contains wildcards or flag "extractarchive" is used but "download" is not';
   SCompilerFilesParamRequiresFlag = 'Parameter "%s" may only be used when the "%s" flag is used';
   SCompilerFilesParamRequiresFlag = 'Parameter "%s" may only be used when the "%s" flag is used';
   SCompilerFilesParamFlagConflict = 'Parameter "%s" may not be used when the "%s" flag is used';
   SCompilerFilesParamFlagConflict = 'Parameter "%s" may not be used when the "%s" flag is used';
   SCompilerFilesParamFlagConflictSameSource = 'Parameter "%s" and the "%s" flag cannot both be used on a single source file';
   SCompilerFilesParamFlagConflictSameSource = 'Parameter "%s" and the "%s" flag cannot both be used on a single source file';

+ 2 - 4
Projects/Src/Compiler.SetupCompiler.pas

@@ -5482,13 +5482,11 @@ begin
             AbortCompileFmt(SCompilerParamFlagMissing, ['external', 'download']);
             AbortCompileFmt(SCompilerParamFlagMissing, ['external', 'download']);
           if not(foIgnoreVersion in Options) then
           if not(foIgnoreVersion in Options) then
             AbortCompileFmt(SCompilerParamFlagMissing, ['ignoreversion', 'download']);
             AbortCompileFmt(SCompilerParamFlagMissing, ['ignoreversion', 'download']);
-          if foExtractArchive in Options then
-            AbortCompileFmt(SCompilerParamErrorBadCombo2, [ParamCommonFlags, 'download', 'extractarchive']);
           if foCompareTimeStamp in Options then
           if foCompareTimeStamp in Options then
             AbortCompileFmt(SCompilerParamErrorBadCombo2, [ParamCommonFlags, 'download', 'comparetimestamp']);
             AbortCompileFmt(SCompilerParamErrorBadCombo2, [ParamCommonFlags, 'download', 'comparetimestamp']);
           if foSkipIfSourceDoesntExist in Options then
           if foSkipIfSourceDoesntExist in Options then
             AbortCompileFmt(SCompilerParamErrorBadCombo2, [ParamCommonFlags, 'download', 'skipifsourcedoesntexist']);
             AbortCompileFmt(SCompilerParamErrorBadCombo2, [ParamCommonFlags, 'download', 'skipifsourcedoesntexist']);
-          if RecurseSubdirs then
+            if not(foExtractArchive in Options) and RecurseSubdirs then
             AbortCompileFmt(SCompilerParamErrorBadCombo2, [ParamCommonFlags, 'recursesubdirs', 'download']);
             AbortCompileFmt(SCompilerParamErrorBadCombo2, [ParamCommonFlags, 'recursesubdirs', 'download']);
           if ADestName = '' then
           if ADestName = '' then
             AbortCompileFmt(SCompilerParamFlagMissingParam, ['DestName', 'download']);
             AbortCompileFmt(SCompilerParamFlagMissingParam, ['DestName', 'download']);
@@ -5542,7 +5540,7 @@ begin
             Include(Options, foRecurseSubDirsExternal);
             Include(Options, foRecurseSubDirsExternal);
           CheckConst(SourceWildcard, MinVersion, []);
           CheckConst(SourceWildcard, MinVersion, []);
         end;
         end;
-        if (ADestName <> '') and (SourceIsWildcard or (foExtractArchive in Options)) then
+        if (ADestName <> '') and (SourceIsWildcard or (not (foDownload in Options) and (foExtractArchive in Options))) then
           AbortCompile(SCompilerFilesDestNameCantBeSpecified);
           AbortCompile(SCompilerFilesDestNameCantBeSpecified);
         CheckConst(ADestDir, MinVersion, []);
         CheckConst(ADestDir, MinVersion, []);
         ADestDir := AddBackslash(ADestDir);
         ADestDir := AddBackslash(ADestDir);

+ 3 - 0
Projects/Src/Setup.Install.pas

@@ -2174,6 +2174,9 @@ var
               SetProgress(ProgressBefore);
               SetProgress(ProgressBefore);
               ExpectedBytesLeft := CurFile^.ExternalSize;
               ExpectedBytesLeft := CurFile^.ExternalSize;
               if foDownload in CurFile^.Options then begin
               if foDownload in CurFile^.Options then begin
+                { Archive download should have been done already by Setup.WizardForm's DownloadArchivesToExtract }
+                if foExtractArchive in CurFile^.Options then
+                  InternalError('Unexpected Download flag');
                 if foSkipIfSourceDoesntExist in CurFile^.Options then
                 if foSkipIfSourceDoesntExist in CurFile^.Options then
                   InternalError('Unexpected SkipIfSourceDoesntExist flag');
                   InternalError('Unexpected SkipIfSourceDoesntExist flag');
                 if not(foCustomDestName in CurFile^.Options) then
                 if not(foCustomDestName in CurFile^.Options) then

+ 3 - 0
Projects/Src/Setup.MainFunc.pas

@@ -1868,6 +1868,9 @@ begin
         else begin
         else begin
           { External file }
           { External file }
           if foDownload in CurFile^.Options then begin
           if foDownload in CurFile^.Options then begin
+           { Archive download should have been done already by Setup.WizardForm's DownloadArchivesToExtract }
+            if foExtractArchive in CurFile^.Options then
+              InternalError('Unexpected Download flag');
             if not(foCustomDestName in CurFile^.Options) then
             if not(foCustomDestName in CurFile^.Options) then
               InternalError('Expected CustomDestName flag');
               InternalError('Expected CustomDestName flag');
             { CurFile^.DestName now includes a a filename, see TSetupCompiler.EnumFilesProc.ProcessFileList }
             { CurFile^.DestName now includes a a filename, see TSetupCompiler.EnumFilesProc.ProcessFileList }

+ 11 - 3
Projects/Src/Setup.ScriptDlg.pas

@@ -200,7 +200,9 @@ type
         const AllowedKeysRuntimeIDs: TStringList): Integer;
         const AllowedKeysRuntimeIDs: TStringList): Integer;
       function AddEx(const Url, BaseName, RequiredSHA256OfFile, UserName, Password: String): Integer;
       function AddEx(const Url, BaseName, RequiredSHA256OfFile, UserName, Password: String): Integer;
       function AddExWithISSigVerify(const Url, ISSigUrl, BaseName, UserName, Password: String;
       function AddExWithISSigVerify(const Url, ISSigUrl, BaseName, UserName, Password: String;
-        const AllowedKeysRuntimeIDs: TStringList): Integer;
+        const AllowedKeysRuntimeIDs: TStringList): Integer; overload;
+      function AddExWithISSigVerify(const Url, ISSigUrl, BaseName, UserName, Password: String;
+        const ISSigAllowedKeys: AnsiString): Integer; overload;
       procedure Clear;
       procedure Clear;
       function Download: Int64;
       function Download: Int64;
       property OnDownloadProgress: TOnDownloadProgress write FOnDownloadProgress;
       property OnDownloadProgress: TOnDownloadProgress write FOnDownloadProgress;
@@ -986,7 +988,7 @@ begin
     else
     else
       Log(Format('  %d bytes done.', [Progress]));
       Log(Format('  %d bytes done.', [Progress]));
 
 
-    FMsg2Label.Caption := IfThen(FShowBaseNameInsteadOfUrl, BaseName, Url);
+    FMsg2Label.Caption := IfThen(FShowBaseNameInsteadOfUrl, PathExtractName(BaseName), Url);
     if ProgressMax > MaxLongInt then begin
     if ProgressMax > MaxLongInt then begin
       Progress32 := Round((Progress / ProgressMax) * MaxLongInt);
       Progress32 := Round((Progress / ProgressMax) * MaxLongInt);
       ProgressMax32 := MaxLongInt;
       ProgressMax32 := MaxLongInt;
@@ -1117,8 +1119,14 @@ end;
 function TDownloadWizardPage.AddExWithISSigVerify(const Url, ISSigUrl, BaseName, UserName,
 function TDownloadWizardPage.AddExWithISSigVerify(const Url, ISSigUrl, BaseName, UserName,
   Password: String; const AllowedKeysRuntimeIDs: TStringList): Integer;
   Password: String; const AllowedKeysRuntimeIDs: TStringList): Integer;
 begin
 begin
-  { Also see Setup.ScriptFunc DownloadTemporaryFileWithISSigVerify }
   const ISSigAllowedKeys = ConvertAllowedKeysRuntimeIDsToISSigAllowedKeys(AllowedKeysRuntimeIDs);
   const ISSigAllowedKeys = ConvertAllowedKeysRuntimeIDsToISSigAllowedKeys(AllowedKeysRuntimeIDs);
+  AddExWithISSigVerify(Url, ISSigUrl, BaseName, UserName, Password, ISSigAllowedKeys);
+end;
+
+function TDownloadWizardPage.AddExWithISSigVerify(const Url, ISSigUrl, BaseName, UserName,
+  Password: String; const ISSigAllowedKeys: AnsiString): Integer;
+begin
+  { Also see Setup.ScriptFunc DownloadTemporaryFileWithISSigVerify }
   DoAdd(GetISSigUrl(Url, ISSigUrl), BaseName + ISSigExt, '', UserName, Password, False, '');
   DoAdd(GetISSigUrl(Url, ISSigUrl), BaseName + ISSigExt, '', UserName, Password, False, '');
   Result := DoAdd(Url, BaseName, '', UserName, Password, True, ISSigAllowedKeys);
   Result := DoAdd(Url, BaseName, '', UserName, Password, True, ISSigAllowedKeys);
 end;
 end;

+ 122 - 24
Projects/Src/Setup.WizardForm.pas

@@ -188,6 +188,7 @@ type
     PrepareToInstallNeedsRestart: Boolean;
     PrepareToInstallNeedsRestart: Boolean;
     EnableAnchorOuterPagesOnResize: Boolean;
     EnableAnchorOuterPagesOnResize: Boolean;
     EnableAdjustReadyLabelHeightOnResize: Boolean;
     EnableAdjustReadyLabelHeightOnResize: Boolean;
+    FDownloadArchivesPage: TWizardPage; { TWizardPage to avoid circular reference. Is always a TDownloadWizardPage. }
     procedure AdjustFocus;
     procedure AdjustFocus;
     procedure AnchorOuterPages;
     procedure AnchorOuterPages;
     procedure CalcCurrentComponentsSpace;
     procedure CalcCurrentComponentsSpace;
@@ -342,11 +343,12 @@ function ValidateCustomDirEdit(const AEdit: TEdit;
 implementation
 implementation
 
 
 uses
 uses
-  ShellApi, ShlObj, Types,
-  PathFunc, RestartManager,
+  ShellApi, ShlObj, Types, Generics.Collections,
+  PathFunc, RestartManager, SHA256,
   SetupLdrAndSetup.Messages, Setup.MainForm, Setup.MainFunc, Shared.CommonFunc.Vcl,
   SetupLdrAndSetup.Messages, Setup.MainForm, Setup.MainFunc, Shared.CommonFunc.Vcl,
   Shared.CommonFunc, Setup.InstFunc, Setup.SelectFolderForm, Setup.FileExtractor,
   Shared.CommonFunc, Setup.InstFunc, Setup.SelectFolderForm, Setup.FileExtractor,
-  Setup.LoggingFunc, Setup.ScriptRunner, Shared.SetupTypes, Shared.SetupSteps;
+  Setup.LoggingFunc, Setup.ScriptRunner, Shared.SetupTypes, Shared.SetupSteps,
+  Setup.ScriptDlg, SetupLdrAndSetup.InstFunc, Setup.Install;
 
 
 {$R *.DFM}
 {$R *.DFM}
 
 
@@ -1325,6 +1327,7 @@ begin
   FreeAndNil(PrevSelectedComponents);
   FreeAndNil(PrevSelectedComponents);
   FreeAndNil(InitialSelectedComponents);
   FreeAndNil(InitialSelectedComponents);
   FreeAndNil(FPageList);
   FreeAndNil(FPageList);
+  FreeAndNil(FDownloadArchivesPage);
   inherited;
   inherited;
 end;
 end;
 
 
@@ -1819,6 +1822,91 @@ begin
 end;
 end;
 
 
 function TWizardForm.PrepareToInstall(const WizardComponents, WizardTasks: TStringList): String;
 function TWizardForm.PrepareToInstall(const WizardComponents, WizardTasks: TStringList): String;
+
+  function GetClearedDownloadArchivesPage: TDownloadWizardPage;
+  begin
+    if FDownloadArchivesPage = nil then begin
+      Result := TDownloadWizardPage.Create(Self);
+      try
+        Result.Caption := SetupMessages[msgWizardPreparing];
+        Result.Description := SetupMessages[msgPreparingDesc];
+        Result.ShowBaseNameInsteadOfUrl := True;
+        AddPage(Result, -1);
+        Result.Initialize;
+        FDownloadArchivesPage := Result;
+      except
+        FreeAndNil(Result);
+        raise;
+      end;
+    end else begin
+      Result := FDownloadArchivesPage as TDownloadWizardPage;
+      Result.Clear;
+    end;
+  end;
+
+  procedure DownloadArchivesToExtract;
+  begin
+    var DownloadPage: TDownloadWizardPage := nil;
+
+    const ArchivesToDownload = TDictionary<Integer, String>.Create;
+    try
+      for var I := 0 to Entries[seFile].Count-1 do begin
+        with PSetupFileEntry(Entries[seFile][I])^ do begin
+          if (foDownload in Options) and (foExtractArchive in Options) then begin
+            if DownloadPage = nil then
+              DownloadPage := GetClearedDownloadArchivesPage;
+            if not(foCustomDestName in Options) then
+              InternalError('Expected CustomDestName flag');
+            { Prepare }
+            const TempDir = AddBackslash(TempInstallDir);
+            const DestDir = GenerateUniqueName(False, TempDir + '_isetup', '.tmp');
+            const DestFile = AddBackslash(DestDir) + PathExtractName(DestName);
+            const BaseName = Copy(DestFile, Length(TempDir)+1, MaxInt);
+             { Add to ArchivesToDownload }
+            ArchivesToDownload.Add(I, DestFile);
+            { Add to DownloadPage }
+            const SourceFile = ExpandConst(SourceFilename);
+            const UserName = ExpandConst(DownloadUserName);
+            const Password = ExpandConst(DownloadPassword);
+            if Verification.Typ = fvISSig then begin
+              const ISSigUrl = GetISSigUrl(SourceFile, ExpandConst(DownloadISSigSource));
+              DownloadPage.AddExWithISSigVerify(SourceFile, ISSigUrl, BaseName, UserName, Password, Verification.ISSigAllowedKeys)
+            end else begin
+              var RequiredSHA256OfFile: String;
+              if Verification.Typ = fvHash then
+                RequiredSHA256OfFile := SHA256DigestToString(Verification.Hash)
+              else
+                RequiredSHA256OfFile := '';
+              DownloadPage.AddEx(SourceFile, BaseName, RequiredSHA256OfFile, UserName, Password);
+            end;
+          end;
+        end;
+      end;
+
+      if DownloadPage <> nil then begin
+        DownloadPage.Show;
+        try
+          DownloadPage.Download;
+          for var A in ArchivesToDownload do begin
+            with PSetupFileEntry(Entries[seFile][A.Key])^ do begin
+              SourceFilename := A.Value;
+              { Remove Download flag since download has been done, and remove CustomDestName flag
+                since ExtractArchive flag doesn't like that }
+              Options := Options - [foDownload, foCustomDestName];
+              { DestName should now not include a filename, see TSetupCompiler.EnumFilesProc.ProcessFileList }
+              DestName := PathExtractPath(DestName);
+              Verification.Typ := fvNone;
+            end;
+          end;
+        finally
+          DownloadPage.Hide;
+        end;
+      end;
+    finally
+      ArchivesToDownload.Free;
+    end;
+  end;
+
 var
 var
   CodeNeedsRestart: Boolean;
   CodeNeedsRestart: Boolean;
   Y: Integer;
   Y: Integer;
@@ -1830,29 +1918,39 @@ begin
   PreparingYesRadio.Visible := False;
   PreparingYesRadio.Visible := False;
   PreparingNoRadio.Visible := False;
   PreparingNoRadio.Visible := False;
   PreparingMemo.Visible := False;
   PreparingMemo.Visible := False;
-  if not PreviousInstallCompleted(WizardComponents, WizardTasks) then begin
-    Result := ExpandSetupMessage(msgPreviousInstallNotCompleted);
-    PrepareToInstallNeedsRestart := True;
-  end else if (CodeRunner <> nil) and CodeRunner.FunctionExists('PrepareToInstall', True) then begin
-    SetCurPage(wpPreparing);
-    BackButton.Visible := False;
-    NextButton.Visible := False;
-    CancelButton.Enabled := False;
-    if InstallMode = imSilent then
-      WizardForm.Visible := True;
-    WizardForm.Update;
-    try
-      DownloadTemporaryFileOrExtractArchiveProcessMessages := True;
-      CodeNeedsRestart := False;
-      Result := CodeRunner.RunStringFunctions('PrepareToInstall', [@CodeNeedsRestart], bcNonEmpty, True, '');
-      PrepareToInstallNeedsRestart := (Result <> '') and CodeNeedsRestart;
-    finally
-      DownloadTemporaryFileOrExtractArchiveProcessMessages := False;
-      UpdateCurPageButtonState;
+
+  try
+    DownloadArchivesToExtract;
+  except
+    Result := GetExceptMessage;
+  end;
+
+  if Result = '' then begin
+    if not PreviousInstallCompleted(WizardComponents, WizardTasks) then begin
+      Result := ExpandSetupMessage(msgPreviousInstallNotCompleted);
+      PrepareToInstallNeedsRestart := True;
+    end else if (CodeRunner <> nil) and CodeRunner.FunctionExists('PrepareToInstall', True) then begin
+      SetCurPage(wpPreparing);
+      BackButton.Visible := False;
+      NextButton.Visible := False;
+      CancelButton.Enabled := False;
+      if InstallMode = imSilent then
+        WizardForm.Visible := True;
+      WizardForm.Update;
+      try
+        DownloadTemporaryFileOrExtractArchiveProcessMessages := True;
+        CodeNeedsRestart := False;
+        Result := CodeRunner.RunStringFunctions('PrepareToInstall', [@CodeNeedsRestart], bcNonEmpty, True, '');
+        PrepareToInstallNeedsRestart := (Result <> '') and CodeNeedsRestart;
+      finally
+        DownloadTemporaryFileOrExtractArchiveProcessMessages := False;
+        UpdateCurPageButtonState;
+      end;
+      if WindowState <> wsMinimized then  { VCL bug workaround }
+        Application.BringToFront;
     end;
     end;
-    if WindowState <> wsMinimized then  { VCL bug workaround }
-      Application.BringToFront;
   end;
   end;
+
   if Result <> '' then begin
   if Result <> '' then begin
     if PrepareToInstallNeedsRestart then
     if PrepareToInstallNeedsRestart then
       PreparingLabel.Caption := Result +
       PreparingLabel.Caption := Result +