Building memo text for Inno Download Plugin - inno-setup

I have reviewed the help documentation for IDP and I cannot find any functions for building the memo text of the files to download.
Previously I was using DwinsHs and it has been giving me problems. But I was able to use:
function DwinsHs_MemoDownloadInfo(Space, NewLine: String): String;
var
i: Integer;
begin
Result := '';
for i := 0 to GetArrayLength(DwinsHs_DownloadsList) - 1 do
begin
if DwinsHs_DownloadsList[i].Required then
begin
Result := Result + Space + ExtractFileName(DwinsHs_DownloadsList[i].Filename);
if DwinsHs_DownloadsList[i].Downloaded then
begin
Result := Result + Space + ExpandConstant('{cm:ReadyMemo_Downloaded}');
end;
Result := Result + NewLine;
end;
end;
if Result <> '' then
begin
Result := ExpandConstant('{cm:ReadyMemo_Download}') + NewLine + Result;
end;
end;
So, potentially we have up to 4 items that will be downloaded:
Help Documentation setup
VC Redist x86
VC Redist x64
Dot Net Framework
The relevant files are added using idpAddFile (although I don't specify file sizes so there is a little delay). I have asked it to show the download page after wpPreparing:
idpDownloadAfter(wpPreparing);
Ideally, on the memo page I would like it to list the files that we have determined the user wants to download.

You know what files you are downloading, so collect their names at the time you are calling idpAddFile. You can make a wrapper function as a replacement for idpAddFile.
var
FilesToDownload: string;
procedure AddFileForDownload(Url, Filename: string);
begin
idpAddFile(Url, Filename);
FilesToDownload := FilesToDownload + ' ' + ExtractFileName(FileName) + #13#10;
end;

Related

Parse key-value text file in Inno Setup for checking version number

I'm creating a Inno Setup installer/updater for my application. Now I need to find a way to check if a new version is available and if it is available it should be installed automatically over the already installed version.
The special case is that the version number is in a file with other data.
The file that Inno Setup need to read looks like:
#Eclipse Product File
#Fri Aug 18 08:20:35 CEST 2017
version=0.21.0
name=appName
id=appId
I already found a way to update the application using a script that only read a text file with the version number in it.
Inno setup: check for new updates
But in my case it contains more data that the installer does not need. Can someone help me to build a script that can parse the version number out of the file?
The code that I already have looks like:
function GetInstallDir(const FileName, Section: string): string;
var
S: string;
DirLine: Integer;
LineCount: Integer;
SectionLine: Integer;
Lines: TArrayOfString;
begin
Result := '';
Log('start');
if LoadStringsFromFile(FileName, Lines) then
begin
Log('Loaded file');
LineCount := GetArrayLength(Lines);
for SectionLine := 0 to LineCount - 1 do
Log('File line ' + lines[SectionLine]);
if (pos('version=', Lines[SectionLine]) <> 0) then
begin
Log('version found');
S := RemoveQuotes(Trim(Lines[SectionLine]));
StringChangeEx(S, '\\', '\', True);
Result := S;
Exit;
end;
end;
end;
But when running the script the check for checking if the version string is on the line does not work.
Your code is almost correct. You are only missing begin and end around the code, that you want to repeat in the for loop. So only the Log line repeats; and the if is executed for out-of-the-range LineCount index.
It becomes obvious, if you format the code better:
function GetInstallDir(const FileName, Section: string): string;
var
S: string;
DirLine: Integer;
LineCount: Integer;
SectionLine: Integer;
Lines: TArrayOfString;
begin
Result := '';
Log('start');
if LoadStringsFromFile(FileName, Lines) then
begin
Log('Loaded file');
LineCount := GetArrayLength(Lines);
for SectionLine := 0 to LineCount - 1 do
begin { <--- Missing }
Log('File line ' + lines[SectionLine] );
if (pos('version=', Lines[SectionLine]) <> 0) then
begin
Log('version found');
S := RemoveQuotes(Trim(Lines[SectionLine]));
StringChangeEx(S, '\\', '\', True);
Result := S;
Exit;
end;
end; { <--- Missing }
end;
end;

Inno Setup - Cyrillic String shows up as question marks

I am running this code:
function CmdLineParamExists(const Value: string): Boolean;
var
I: Integer;
begin
Result := False;
for I := 1 to ParamCount do
begin
if CompareText(Copy(ParamStr(I), 1, Length(Value)), Value) = 0 then
begin
Result := True;
Exit;
end;
end;
end;
function GetAppName(Value: string): string;
begin
if CmdLineParamExists('/COMPONENTS=prog2') then
begin
Result := 'Программа 2'; //<----This shows up as ????????? 2
end
else
begin
Result := '{#SetupSetting("AppName")}';
end;
end;
procedure CurPageChanged(CurPageID: Integer);
var
S: string;
Begin
if CurPageID = wpSelectDir then
begin
S := SetupMessage(msgSelectDirLabel3);
StringChange(S, '[name]', GetAppName(''));
WizardForm.SelectDirLabel.Caption := S;
end;
end;
Now, I'm not sure what I'm doing wrong here. Every other string shows up correctly, except when I use the result of GetAppName. Should I convert anything to AnsiString at some point?
I'm assuming that you are using Ansi version of Inno Setup.
In the Ansi version, the culprit is probably the StringChange as it does not play nicely with non-Ansi character sets. Try using StringChangeEx.
Though you should be using Unicode version of Inno Setup anyway.
Only the most recent version of Inno Setup, 5.6, does support Unicode string literals. So make sure you have the latest version.
If you are stuck with an older version:
Encode the string like
#$041F#$0440#$043E#$0433#$0440#$0430#$043C#$043C#$0430 + ' 2'
Or, actually the most correct way is to add a new custom message to the language files (like Russian.isl):
[CustomMessages]
Program2=Программа 2
And load it like:
CustomMessage('Program2')

Inno Setup get directory size including subdirectories

I am trying to write a function that returns the size of a directory. I have written the following code, but it is not returning the correct size. For example, when I run it on the {pf} directory it returns 174 bytes, which is clearly wrong as this directory is multiple Gigabytes in size. Here is the code I have:
function GetDirSize(DirName: String): Int64;
var
FindRec: TFindRec;
begin
if FindFirst(DirName + '\*', FindRec) then
begin
try
repeat
Result := Result + (Int64(FindRec.SizeHigh) shl 32 + FindRec.SizeLow);
until not FindNext(FindRec);
finally
FindClose(FindRec);
end;
end
else
begin
Result := -1;
end;
end;
I suspect that the FindFirst function does not include subdirectories, which is why I am not getting the correct result. Therefore, how can I return the correct size of a directory i.e. including all files in all subdirectories, the same as selecting Properties on a Folder in Windows Explorer? I am using FindFirst as the function needs to support directory sizes over 2GB.
The FindFirst does include subdirectories, but it won't get you their sizes.
You have to recurse into subdirectories and calculate the total size file by file, similarly as to for example Inno Setup: copy folder, subfolders and files recursively in Code section.
function GetDirSize(Path: String): Int64;
var
FindRec: TFindRec;
FilePath: string;
Size: Int64;
begin
if FindFirst(Path + '\*', FindRec) then
begin
Result := 0;
try
repeat
if (FindRec.Name <> '.') and (FindRec.Name <> '..') then
begin
FilePath := Path + '\' + FindRec.Name;
if (FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY) <> 0 then
begin
Size := GetDirSize(FilePath);
end
else
begin
Size := Int64(FindRec.SizeHigh) shl 32 + FindRec.SizeLow;
end;
Result := Result + Size;
end;
until not FindNext(FindRec);
finally
FindClose(FindRec);
end;
end
else
begin
Log(Format('Failed to list %s', [Path]));
Result := -1;
end;
end;
For Int64, you need Unicode version of Inno Setup, what you should be using in any case. Only if you have a very good reason to stick with Ansi version, you can replace the Int64 with Integer, but than you are limited to 2 GB.

Inno Setup: How do I create a 'check:' function for currently installed version of DirectX and whether MS VC++ 2005 is installed

I'm in the process of creating a custom installer and for the most part I have it set up the way I want it, except that it's missing two features I want to add to the Setup. I've done some extensive searching and while I've found plenty of similar questions, I haven't been able to take the responses to those and modify them for my specific needs with any success.
Basically what I need to do is create a custom function for 'Check:' that checks the version of DirectX that's currently installed. I know there is the 'RegQueryStringValue' function, and I know where the key is in the registry that contains the version (HKLM\SOFTWARE\Microsoft\DirectX, Version). I just don't know how to implement the code to check the version contained in the registry, and if it reports back a value less than 4.09.00.0904 to go ahead with the DXSETUP I have entered under [Files].
I also want to perform this same routine for a 'Check:' to use with Visual C++ 2005 (x86). This one I believe will be simpler as it only needs to check if an actual key exists (RegQueryKey?) and not a value. I believe the key for VC++ 2005 is HKLM\SOFTWARE\Microsoft\VisualStudio\8.0
If anyone can help me out I'd greatly appreciate it, as I've been messing with this for several hours trying to get something functional together without much success. If you require any further information from me I'd be more than happy to provide it.
There's an example for checking for prerequisites included in the Inno Setup Examples of doing this sort of thing in CodePrepareToInstall.iss. InitializeSetup shows how to check for the existence of a registry entry, and you can do so in DetectAndInstallPrerequisites. I added a CheckDXVersion function that you can pass the Version string from the DirectX registry entry that checks for 4.9 or higher (untested!) you can use as well.
; -- CodePrepareToInstall.iss --
;
; This script shows how the PrepareToInstall event function can be used to
; install prerequisites and handle any reboots in between, while remembering
; user selections across reboots.
[Setup]
AppName=My Program
AppVersion=1.5
DefaultDirName={pf}\My Program
DefaultGroupName=My Program
UninstallDisplayIcon={app}\MyProg.exe
OutputDir=userdocs:Inno Setup Examples Output
[Files]
Source: "MyProg.exe"; DestDir: "{app}";
Source: "MyProg.chm"; DestDir: "{app}";
Source: "Readme.txt"; DestDir: "{app}"; Flags: isreadme;
[Icons]
Name: "{group}\My Program"; Filename: "{app}\MyProg.exe"
[Code]
const
(*** Customize the following to your own name. ***)
RunOnceName = 'My Program Setup restart';
QuitMessageReboot = 'The installation of a prerequisite program was not completed. You will need to restart your computer to complete that installation.'#13#13'After restarting your computer, Setup will continue next time an administrator logs in.';
QuitMessageError = 'Error. Cannot continue.';
var
Restarted: Boolean;
function InitializeSetup(): Boolean;
begin
Restarted := ExpandConstant('{param:restart|0}') = '1';
if not Restarted then begin
Result := not RegValueExists(HKLM, 'Software\Microsoft\Windows\CurrentVersion\RunOnce', RunOnceName);
if not Result then
MsgBox(QuitMessageReboot, mbError, mb_Ok);
end else
Result := True;
end;
function CheckDXVersion(const VerString: String): Boolean;
var
MajorVer, MinorVer: Integer;
StartPos: Integer;
TempStr: string;
begin
(* Extract major version *)
StartPos := Pos('.', VerString);
MajorVer := StrToInt(Copy(VerString, 1, StartPos - 1);
(* Remove major version and decimal point that follows *)
TempStr := Copy(VerString, StartPos + 1, MaxInt);
(* Find next decimal point *)
StartPos := Pos('.', TempStr);
(* Extract minor version *)
MinorVer := Copy(TempStr, 1, StartPos - 1);
Result := (MajorVer > 4) or ((MajorVer = 4) and MinorVer >= 9));
end;
function DetectAndInstallPrerequisites: Boolean;
begin
(*** Place your prerequisite detection and installation code below. ***)
(*** Return False if missing prerequisites were detected but their installation failed, else return True. ***)
//<your code here>
Result := True;
(*** Remove the following block! Used by this demo to simulate a prerequisite install requiring a reboot. ***)
if not Restarted then
RestartReplace(ParamStr(0), '');
end;
function Quote(const S: String): String;
begin
Result := '"' + S + '"';
end;
function AddParam(const S, P, V: String): String;
begin
if V <> '""' then
Result := S + ' /' + P + '=' + V;
end;
function AddSimpleParam(const S, P: String): String;
begin
Result := S + ' /' + P;
end;
procedure CreateRunOnceEntry;
var
RunOnceData: String;
begin
RunOnceData := Quote(ExpandConstant('{srcexe}')) + ' /restart=1';
RunOnceData := AddParam(RunOnceData, 'LANG', ExpandConstant('{language}'));
RunOnceData := AddParam(RunOnceData, 'DIR', Quote(WizardDirValue));
RunOnceData := AddParam(RunOnceData, 'GROUP', Quote(WizardGroupValue));
if WizardNoIcons then
RunOnceData := AddSimpleParam(RunOnceData, 'NOICONS');
RunOnceData := AddParam(RunOnceData, 'TYPE', Quote(WizardSetupType(False)));
RunOnceData := AddParam(RunOnceData, 'COMPONENTS', Quote(WizardSelectedComponents(False)));
RunOnceData := AddParam(RunOnceData, 'TASKS', Quote(WizardSelectedTasks(False)));
(*** Place any custom user selection you want to remember below. ***)
//<your code here>
RegWriteStringValue(HKLM, 'Software\Microsoft\Windows\CurrentVersion\RunOnce', RunOnceName, RunOnceData);
end;
function PrepareToInstall(var NeedsRestart: Boolean): String;
var
ChecksumBefore, ChecksumAfter: String;
begin
ChecksumBefore := MakePendingFileRenameOperationsChecksum;
if DetectAndInstallPrerequisites then begin
ChecksumAfter := MakePendingFileRenameOperationsChecksum;
if ChecksumBefore <> ChecksumAfter then begin
CreateRunOnceEntry;
NeedsRestart := True;
Result := QuitMessageReboot;
end;
end else
Result := QuitMessageError;
end;
function ShouldSkipPage(PageID: Integer): Boolean;
begin
Result := Restarted;
end;

Read modified time from one file and use it to set the time for files in a whole directory

In my installer I'm extracting files from archives that don't store time/date attributes, so when they're extracted the last modified date is set to the current date. I would like to set it to the last modified date of the archive file but I can't figure out how. I tried using pieces of the code from here and here but while it didn't give any errors, it didn't work for changing the time. Last modified date would need to be changed for * .* in a folder.
Also, where do I need to hook into to delete these files if the user cancels setup and it starts rolling back changes? I've got it taken care of in UninstallDelete but not if the user cancels setup.
EDIT: Disregard the second part, I actually figured it out shortly after I posted here. Added my own CleanUp() to DeinitializeSetup() with a check for the uninstaller registry key.
Here is the section of code I'm trying to add it to:
procedure VolExtract(VWorld: String);
var
ResultCode: Integer;
VPath: String;
begin
// Files are extracted to {app}\VWorld\One, {app}\VWorld\Two, etc.
VPath := ExpandConstant('{app}\' + VWorld);
WizardForm.FilenameLabel.Caption := DriveLetter + VWorld + '\one.vol';
if Exec(ExpandConstant('{tmp}\volextract.exe'), '"' + DriveLetter + VWorld + '\one.vol" "' + VPath + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0) then
begin
// Yep, it executed successfully
WizardForm.FilenameLabel.Caption := DriveLetter + VWorld + '\two.vol';
if Exec(ExpandConstant('{tmp}\volextract.exe'), '"' + DriveLetter + VWorld + '\two.vol" "' + VPath + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0) then
begin
// Next
WizardForm.FilenameLabel.Caption := DriveLetter + VWorld + '\three.vol';
if Exec(ExpandConstant('{tmp}\volextract.exe'), '"' + DriveLetter + VWorld + '\three.vol" "' + VPath + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0) then
begin
// Next
WizardForm.FilenameLabel.Caption := DriveLetter + VWorld + '\four.vol';
Exec(ExpandConstant('{tmp}\volextract.exe'), '"' + DriveLetter + VWorld + '\four.vol" "' + VPath + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
end;
end;
end;
if ResultCode <> 0 then
begin
// Handle Fail
CDFound := False;
MsgBox(CustomMessage('FileErr'), mbInformation, MB_OK);
WizardForm.Close;
end;
end;
To change the last modified time (let's call it LastWriteTime for now) for all files from a specified directory by the LastWriteTime of a certain file, use the following code after you have your files extracted. You can follow the commented version of the previous version of this post, but note that I've had bugs there (mixed time parameters and unused file flag parameter), but the point remains.
Also note that this code is for ANSI version of InnoSetup. If you need to use this for Unicode version, you should define the CreateFile function import as CreateFileW instead of CreateFileA or use the trick suggested by kobik in this post.
[code]
const
OPEN_EXISTING = 3;
FILE_SHARE_WRITE = 2;
GENERIC_WRITE = $40000000;
INVALID_HANDLE_VALUE = 4294967295;
function CreateFile(lpFileName: string; dwDesiredAccess, dwShareMode,
lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes: DWORD;
hTemplateFile: THandle): THandle;
external 'CreateFileA#kernel32.dll stdcall';
function CloseHandle(hObject: THandle): BOOL;
external 'CloseHandle#kernel32.dll stdcall';
function SetFileTime(hFile: THandle; const lpCreationTime, lpLastAccessTime,
lpLastWriteTime: TFileTime): BOOL;
external 'SetFileTime#kernel32.dll stdcall';
function FileSetTime(const AFileName: string; const ACreationTime,
ALastAccessTime, ALastWriteTime: TFileTime): Boolean;
var
FileHandle: THandle;
begin
Result := False;
FileHandle := CreateFile(AFileName, GENERIC_WRITE, FILE_SHARE_WRITE, 0,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if FileHandle <> INVALID_HANDLE_VALUE then
try
Result := SetFileTime(FileHandle, ACreationTime, ALastAccessTime,
ALastWriteTime);
finally
CloseHandle(FileHandle);
end;
end;
procedure ModifyLastWriteTime(const ASourceFile, ATargetFolder: string);
var
FindRec: TFindRec;
LastWriteTime: TFileTime;
begin
if FindFirst(ASourceFile, FindRec) then
begin
LastWriteTime := FindRec.LastWriteTime;
if FindFirst(ATargetFolder + '*.*', FindRec) then
try
repeat
if (FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY) = 0 then
FileSetTime(ATargetFolder + FindRec.Name, FindRec.CreationTime,
FindRec.LastAccessTime, LastWriteTime);
until
not FindNext(FindRec);
finally
FindClose(FindRec);
end;
end;
end;
And the usage. The first parameter of the ModifyLastWriteTime procedure is the name of the source file from which the LastWriteTime is taken. The second parameter is the directory in what the files will get modified their LastWriteTime values by the source file (don't forget to have the trailing backslash in the target folder parameter):
ModifyLastWriteTime('c:\SourceFile.xxx', 'c:\TargetFolder\')
About the second question, you can delete the files which have been extracted in a procedure called
Procedure CancelButtonClick(CurPageID: Integer; Var Cancel, Confirm: Boolean);
Begin
End;
As explained in the chm, section Pascal Scripting: Event Functions
About the first question I would suggest you to use inno setup [files] section instead of extracting from an archive. You could probably extract this archive to a local folder (so from your side, before compiling, and add this local folder to the [files]. But I may misunderstand your imperative about the file modification date.

Resources