Install the same file to all subdirectories in Inno Setup - installation

I am newbie with Inno Setup and I have been reading some threads but could not find how to do the following.
I simply would like to search for folders within a directory and in each folder detected install the same file with no selection of wizard page shown to the user. Not recursive, only files inside the detected folders and not subfolders.
I meant to install the same file in all folders detected while giving no option to the user to choose from. However, all other pages in the installer would be displayed as usual.
Thanks in advance

Tag the file with dontcopy flag and then install it programmatically in CurStepChanged(ssInstall) (or ssPostInstall).
Use ExtractTemporaryFile to extract the file to a temporary folder.
Use FindFirst/FindNext functions to find the subfolders.
Use FileCopy to copy the file from a temporary folder to the found subfolder(s).
Log a lot.
This will work well, only if the file is not huge. Otherwise the installer will unpleasantly hang. For a good user experience with huge files, more complex solution is needed.
#define TheFileName "thefile.txt"
[Files]
Source: "{#TheFileName}"; Flags: dontcopy
[Code]
procedure CurStepChanged(CurStep: TSetupStep);
var
RootPath: string;
TempPath: string;
DestPath: string;
FindRec: TFindRec;
Count: Integer;
begin
if CurStep = ssInstall then
begin
Log('Extracting {#TheFileName}...');
ExtractTemporaryFile('{#TheFileName}');
TempPath := ExpandConstant('{tmp}\{#TheFileName}');
RootPath := ExpandConstant('{app}');
Log(Format('Searching in "%s"...', [RootPath]));
Count := 0;
if not FindFirst(RootPath + '\*', FindRec) then
begin
Log(Format('"%s" not found.', [RootPath]));
end
else
begin
try
repeat
if (FindRec.Name <> '.') and (FindRec.Name <> '..') and
(FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY <> 0) then
begin
Log(Format('Found "%s".', [FindRec.Name]));
DestPath := RootPath + '\' + FindRec.Name + '\{#TheFileName}';
if FileCopy(TempPath, DestPath, False) then
begin
Log(Format('The file was installed to "%s".', [DestPath]));
Inc(Count);
end
else
begin
Log(Format('Error installing the file to "%s".', [DestPath]));
end;
end;
until not FindNext(FindRec);
finally
FindClose(FindRec);
end;
if Count = 0 then
begin
Log(Format('No subfolder to install file "%s" to was found in "%s".', [
'{#TheFileName}', RootPath]));
end
else
begin
Log(Format('File "%s" was installed to %d subfolder(s) of "%s".', [
'{#TheFileName}', Count, RootPath]));
end;
end;
end;
end;
Alternatively, if you have a fixed set of folders, you can generate entry for each folder in the [Files] section using preprocessor:
[Files]
#define FolderEntry(Name) \
"Source: ""C:\source\*""; DestDir: ""{app}\" + Name + """; " + \
"Check: CheckDir('" + Name + "')"
#emit FolderEntry('2023')
#emit FolderEntry('2024')
#emit FolderEntry('2025')
[Code]
function CheckDir(DirName: string): Boolean;
begin
Result := DirExists(ExpandConstant('{app}') + '\' + DirName);
end;
If you add SaveToFile to the end of the script:
#expr SaveToFile(AddBackslash(SourcePath) + "Preprocessed.iss")
... then you should see in Preprocessed.iss that the code generates a script like this:
[Files]
Source: "C:\source\*"; DestDir: "{app}\2023"; Check: CheckDir('2023')
Source: "C:\source\*"; DestDir: "{app}\2024"; Check: CheckDir('2024')
Source: "C:\source\*"; DestDir: "{app}\2025"; Check: CheckDir('2025')

Thanks a lot Martin! I did it another way that may be of help to other users.
I set a file for every potential folder I want to detect (it was only for four).
[Files] Source: "C:\Users\XXXXX\dll\*"; DestDir: "{commonappdata}\XXXX\2023"; Check:CheckDir2023;
Then I use the following to check if the folder exists:
function CheckDir2023 : Boolean;
begin
if (DirExists(ExpandConstant('{commonappdata}\xxxxxx\2023\'))) then
begin
Result := True;
end
else
begin
Result := False;
end;
end;

Related

Convert {constants} returned by CurrentFilename in Inno Setup

What to do to get nice, full filename from the code below:
;bugfixes
Source: "Bugfixes\CombatGameConstants.json"; DestDir: "{code:battletechDataDir}\constants";\
Flags: uninsneveruninstall; Components: DataBugfixes; BeforeInstall: BackupFile()
and the BackupFile() procedure:
procedure BackupFile();
var fileToBackup : String;
begin
{ if backup file already exists skip creation, otherwise rename the file to file.backup }
fileToBackup := CurrentFilename(); { get destination file name }
if not FileExists(fileToBackup + '.backup') then
begin
if not RenameFile(fileToBackup, fileToBackup + '.backup') then
MsgBox('Creation backup file for ' + fileToBackup + ' failed!', mbInformation, MB_OK);
end;
end;
This does not convert {code:battletechDataDir} into full path – CurrentFileName() returns me {code:battletechDataDir}\constants\{code:battletechDataDir}\constants. So either how to convert that {code:battletechDataDir} into directory, or backup given file other way?
You can use ExpandConstant function:
fileToBackup := ExpandConstant(CurrentFilename()); { get destination file name }
Though CurrentFilename is intended for use with wildcard source. With a fixed file name, you might as well refer to the file explicitly:
fileToBackup := battletechDataDir('') + '\constants\CombatGameConstants.json';

Inno Setup - Remove path from PATH environment variable while uninstalling a program

I wrote an Inno Setup script which install a program and update the
PATH environment variable with the directory in which the program in installed.
I want to update the PATH environment variable, to restore its previous installation status.
The installation path is chosen by the user while the installer is running.
This is the script, which uses code from How do I modify the PATH environment variable when running an Inno Setup Installer?
[Setup]
ChangesEnvironment=yes
[Registry]
Root: HKLM; \
Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; \
ValueType: expandsz; ValueName: "PATH"; ValueData: "{olddata};{app}"; \
Check: NeedsAddPath('{app}')
Root: HKLM; \
Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; \
ValueName: "PATH"; ValueData: "{app}"; Flags: uninsdeletevalue
[Code]
function NeedsAddPath(Param: string): boolean;
var
OrigPath: string;
begin
if not RegQueryStringValue(HKEY_LOCAL_MACHINE,
'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;
Taking a look to the code, it's possible to note the following instruction:
Root: HKLM; \
Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; \
ValueName: "PATH"; ValueData: "{app}"; Flags: uninsdeletevalue
I used that instruction, adapted (in my opinion) for the my example, reading Inno Setup. How to uninstall registry value?
The use of uninsdeletevalue should be delete the value when the program is uninstalled, and in fact, when I run the uninstaller, the entire PATH variable is deleted, but I need to restore the PATH environment variable to the previous installation value.
I think it's possible reading its value before run the installer, but I don't have any idea to how use it in the uninstall phase.
Can someone help me with a code example?
You cannot have Inno Setup remember the value on installation and restore it, when uninstalling using [Registry] section entry only.
While you can code it, it's not good approach anyway as the PATH likely changes after the installation and you will discard any such changes.
You have to search the PATH for your path and remove the path only.
const
EnvironmentKey = 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment';
procedure RemovePath(Path: string);
var
Paths: string;
P: Integer;
begin
if not RegQueryStringValue(HKLM, EnvironmentKey, 'Path', Paths) then
begin
Log('PATH not found');
end
else
begin
Log(Format('PATH is [%s]', [Paths]));
P := Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';');
if P = 0 then
begin
Log(Format('Path [%s] not found in PATH', [Path]));
end
else
begin
if P > 1 then P := P - 1;
Delete(Paths, P, Length(Path) + 1);
Log(Format('Path [%s] removed from PATH => [%s]', [Path, Paths]));
if RegWriteStringValue(HKLM, EnvironmentKey, 'Path', Paths) then
begin
Log('PATH written');
end
else
begin
Log('Error writing PATH');
end;
end;
end;
end;
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
begin
if CurUninstallStep = usUninstall then
begin
RemovePath(ExpandConstant('{app}'));
end;
end;
The problem seems to stick if a previous version was already installed, containing erroneous uninstall routines, and then updated with the new Setup. The Path was deleted no matter what, even if the Registry section had no Flags anymore. So my task was to write a new Setup that when updating faulty versions makes sure the Path is still there after uninstall.
I came closest with Martin Prikryl's approach, but logging showed I need to save the Path at the beginning of the uninstall (usUninstall) and rewrite it at the end (usPostUninstall), else it wouldn't stick. Also, environment is already errorneous refreshed without Path when processing the usPostUninstall Step, so I added a manual environment refresh which I took from another post.
Here's the final result that worked for me:
var
Paths: string;
const
EnvironmentKey = 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment';
SMTO_ABORTIFHUNG = 2;
WM_WININICHANGE = $001A;
WM_SETTINGCHANGE = WM_WININICHANGE;
type
WPARAM = UINT_PTR;
LPARAM = INT_PTR;
LRESULT = INT_PTR;
function SendTextMessageTimeout(hWnd: HWND; Msg: UINT;
wParam: WPARAM; lParam: PAnsiChar; fuFlags: UINT;
uTimeout: UINT; out lpdwResult: DWORD): LRESULT;
external 'SendMessageTimeoutA#user32.dll stdcall';
procedure SaveOldPath();
begin
if not RegQueryStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths) then
begin
Log('PATH not found');
end else begin
Log(Format('Old Path saved as [%s]', [Paths]));
end;
end;
procedure RemovePath(Path: string);
var
P: Integer;
begin
Log(Format('Prepare to remove from Old PATH [%s]', [Paths]));
P := Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';');
if P = 0 then
begin
Log(Format('Path [%s] not found in PATH', [Path]));
end
else
begin
Delete(Paths, P - 1, Length(Path) + 1);
Log(Format('Path [%s] removed from PATH => [%s]', [Path, Paths]));
if RegWriteExpandStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths) then
begin
Log('PATH written');
end
else
begin
Log('Error writing PATH');
end;
end;
end;
procedure RefreshEnvironment;
var
S: AnsiString;
MsgResult: DWORD;
begin
S := 'Environment';
SendTextMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0,
PAnsiChar(S), SMTO_ABORTIFHUNG, 5000, MsgResult);
end;
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
begin
if CurUninstallStep = usUninstall then
begin
SaveOldPath();
end;
if CurUninstallStep = usPostUninstall then
begin
RemovePath(ExpandConstant('{app}'));
RefreshEnvironment();
end;
end;
These solutions for removing variable from PATH helped a lot, but for me there was a problem in Delete(Paths, P - 1, Length(Path) + 1); when path of my installer was at the beginning. Then Delete function will not work.
My simple modification which worked for me was Delete(Paths, P, Length(Path) + 1);

Inno Setup conditional restart based on result of executable call

My Inno Setup script is used to install a driver. It runs my InstallDriver.exe after this file was copied during step ssInstall.
I need to ask the user to restart in some cases according to the value returned by InstallDriver.exe.
This means that I cannot put InstallDriver.exe in section [Run] because there's no way to monitor it's return value.
So I put it in function CurStepChanged() as follows:
procedure CurStepChanged(CurStep: TSetupStep);
var
TmpFileName, ExecStdout, msg: string;
ResultCode: Integer;
begin
if (CurStep=ssPostInstall) then
begin
Log('CurStepChanged(ssPostInstall)');
TmpFileName := ExpandConstant('{app}') + '\InstallDriver.exe';
if Exec(TmpFileName, 'I', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then .......
However, I can't find a way to make my script restart at this stage.
I thought of using function NeedRestart() to monitor the output of the driver installer, but it is called earlier in the process.
Does it make sense to call the driver installer from within NeedRestart()?
NeedRestart does not look like the right place to install anything. But it would work, as it's fortunately called only once. You will probably want to present a progress somehow though, as the wizard form is almost empty during a call to NeedRestart.
An alternative is to use AfterInstall parameter of the InstallDriver.exe or the driver binary itself (whichever is installed later).
#define InstallDriverName "InstallDriver.exe"
[Files]
Source: "driver.sys"; DestDir: ".."
Source: "{#InstallDriverName}"; DestDir: "{app}"; AfterInstall: InstallDriver
[Code]
var
NeedRestartFlag: Boolean;
const
NeedRestartResultCode = 1;
procedure InstallDriver();
var
InstallDriverPath: string;
ResultCode: Integer;
begin
Log('Installing driver');
InstallDriverPath := ExpandConstant('{app}') + '\{#InstallDriverName}';
if not Exec(InstallDriverPath, 'I', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
begin
Log('Failed to execute driver installation');
end
else
begin
Log(Format('Driver installation finished with code %d', [ResultCode]))
if ResultCode = NeedRestartResultCode then
begin
Log('Need to restart to finish driver installation');
NeedRestartFlag := True;
end;
end;
end;
function NeedRestart(): Boolean;
begin
if NeedRestartFlag then
begin
Log('Need restart');
Result := True;
end
else
begin
Log('Do not need restart');
Result := False;
end;
end;

Disk caching issue with inno-setup?

I will try to be as brief as possible without attaching all the related source files. I have tracked down the issue as much as my Pascal knowledge allows me...
I thing I found a disk caching problem that occurs, for my case, at step ssInstall. I have an installer for an app that, if it finds an older app version installed, it will invoke an uninstallation like this:
procedure CurStepChanged(CurStep: TSetupStep);
var
uninstallStr: String;
ResultCode: Integer;
begin
if (CurStep = ssInstall) and IsUpdatableApplicationInstalled() then
begin
uninstallStr := GetUninstallString();
uninstallStr := RemoveQuotes(uninstallStr);
Result := Exec(uninstallStr, '/SILENT /NORESTART /SUPPRESSMSGBOXES', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
if Result and (ResultCode = 0) then
Log('CurStepChanged = ssInstall; uninstall OK');
//-------------
//Sleep(30000);
//-------------
end;
Also the folders/files are defined like this:
[Dirs]
Name: "{app}\db"; Flags: uninsalwaysuninstall
[Files]
Source: "..\bin\*"; DestDir: "{app}\bin"; Flags: ignoreversion createallsubdirs recursesubdirs
Source: "..\java\jre\*"; DestDir: "{app}\jre"; Flags: ignoreversion recursesubdirs createallsubdirs
blah...
Test case1; Normal installation: Everything goes smoothly.
Log file part:
Starting the installation process.
Creating directory: C:\Program Files <---
Creating directory: C:\Program Files\MyApp <---
Creating directory: C:\Program Files\MyApp\db <---
Creating directory: C:\Program Files\MyApp\jre <---
Creating directory: C:\Program Files\MyApp\jre\lib
Creating directory: C:\Program Files\MyApp\jre\lib\applet
Directory for uninstall files: C:\Program Files\MyApp
Creating new uninstall log: C:\Program Files\MyApp\unins000.dat <--- !!!
-- File entry --
Dest filename: C:\Program Files\MyApp\unins000.exe <--- !!!
blah...
Test case2; Update old version: When getting to step ssInstall, the uninstaller launches, it finishes then the installation begins.
Log file part:
CurStepChanged = ssInstall; uninstall OK
Starting the installation process.
Creating directory: C:\Program Files\MyApp\jre\lib
Creating directory: C:\Program Files\MyApp\jre\lib\applet
Directory for uninstall files: C:\Program Files\MyApp
Creating new uninstall log: C:\Program Files\MyApp\unins001.dat <--- !!!
-- File entry --
Dest filename: C:\Program Files\MyApp\unins001.exe <--- !!!
blah...
As you can see some folders do not get created and my app fails later on when it tries to write to 'db' folder.
If I uncomment the Sleep() command, everything runs smoothly and both the log files are identical.
It seems the disk has enough time to flush the changes! Somehow there must be a flush() command missing in inno-setup.
Can any of the inno-gurus comment or help somehow?
Is there a flush() i could call, instead of the sleep()?
Any help is appreciated. I just want to be sure before I file a bug request.
Just to sum up the comment trail on the question:
Don't Uninstall
The best solution is to not run the uninstaller at all. You can remove redundant files via the [InstallDelete] section; eg. to remove a "jre" subfolder completely (to be replaced as part of your installation) then do this:
[InstallDelete]
Type: filesandordirs; Name: "{app}\jre"
(Only use this for subfolders like this and only sparingly; you can get yourself in trouble if you delete too much stuff.)
For normal individual application files installed by a previous version that are now redundant, you can remove them like so:
[InstallDelete]
Type: files; Name: "{app}\redundant.dll"
(You'll need to do something slightly fancier if they had "regserver" or "sharedfile" when installed.)
If you Uninstall regardless, wait longer
The uninstaller cannot delete itself or the folder where it is located while it is still running. While Inno does take care of this in such a way that it is able to delete the uninstaller and folder, it does mean that your Exec call to the uninstaller will return before this deletion has occurred and before the uninstall process actually finishes.
You will need to wait longer after the Exec for the uninstall to actually finish before you let it continue with the installation. Using a Sleep is simple enough and will work in most cases but if you want the best results you'll need to call into the WinAPI to check the running processes list.
Additionally, you should use the PrepareToInstall event function to perform the actual uninstallation. This will better allow you to handle cases such as uninstall errors or when a reboot is required between uninstall and reinstall. (And because it executes at the "right" time in the installation process.)
This is how I actually worked around this issue. I am posting my solution in hope to help others with the same problem.
Big thanks to all that helped out and especially to Miral for pointing me in the right direction!
The solution is rather simple; wait until the uninstaller exe is deleted!
const
DELAY_MILLIS = 250;
MAX_DELAY_MILLIS = 30000;
function GetUninstallString(): String;
var
uninstallPath: String;
uninstallStr: String;
begin
uninstallPath := ExpandConstant('Software\Microsoft\Windows\CurrentVersion\Uninstall\{#emit SetupSetting("AppId")}_is1');
uninstallStr := '';
if not RegQueryStringValue(HKLM, uninstallPath, 'UninstallString', uninstallStr) then
RegQueryStringValue(HKCU, uninstallPath, 'UninstallString', uninstallStr);
Result := RemoveQuotes(uninstallStr);
end;
function ForceUninstallApplication(): Boolean;
var
ResultCode: Integer;
uninstallStr: String;
delayCounter: Integer;
begin
// 1) Uninstall the application
Log('forcing uninstall of application);
uninstallStr := GetUninstallString();
Result := Exec(uninstallStr, '/SILENT /NORESTART /SUPPRESSMSGBOXES /LOG', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0);
if not Result then
begin
Log('application uninstall failed!');
Exit
end;
Log('application uninstalled!');
// 2) Be sure to wait a while, until the actual uninstaller is deleted!
Log('waiting a while until uninstaller changes are flushed in the filesystem...');
delayCounter := 0;
repeat
Sleep(DELAY_MILLIS);
delayCounter := delayCounter + DELAY_MILLIS;
until not FileExists(uninstallStr) or (delayCounter >= MAX_DELAY_MILLIS);
if (delayCounter >= MAX_DELAY_MILLIS) then
RaiseException('Timeout exceeded trying to delete uninstaller: ' + uninstallStr);
Log('waited ' + IntToStr(delayCounter) + ' milliseconds');
end;
Function ForceUninstallApplication() can be successfuly called from either PrepareToInstall or CurStepChanged(ssInstall). For my case it takes about 500 millis when I use my SSD hard disk and maybe a couple of seconds when I use my external usb hard disk.
Thank you to fubar for this answer ! I just made a little modification in order to avoid the same problem with the install folder which is delete during uninstall process. It could be deleted after the call to ForceUninstallApplication() and avoid the creation of the install folder.
To avoid this problem, I just create a temp file in the install folder and remove when installation in finish. When a unknow file is present folder, unins000.exe don't delete the folder.
Here the fubar code updated:
const
DELAY_MILLIS = 250;
MAX_DELAY_MILLIS = 30000;
var
tempUninstallFilename: String;
function GetUninstallString(): String;
var
uninstallPath: String;
uninstallStr: String;
begin
uninstallPath := ExpandConstant('Software\Microsoft\Windows\CurrentVersion\Uninstall\{#emit SetupSetting("AppId")}_is1');
uninstallStr := '';
if not RegQueryStringValue(HKLM, uninstallPath, 'UninstallString', uninstallStr) then
RegQueryStringValue(HKCU, uninstallPath, 'UninstallString', uninstallStr);
Result := RemoveQuotes(uninstallStr);
end;
function ForceUninstallApplication(): Boolean;
var
ResultCode: Integer;
uninstallStr: String;
delayCounter: Integer;
begin
// 1) Create a temporary file to avoid destruction of install folder during uninstall.
uninstallStr := RemoveQuotes(GetUninstallString());
tempUninstallFilename := ExtractFileDir(uninstallStr) + '\uninstall.log';
SaveStringToFile(tempUninstallFilename, '...', False);
Log('Create temp file: ' + tempUninstallFilename);
// 2) Uninstall the application
Log('forcing uninstall of application);
uninstallStr := GetUninstallString();
Result := Exec(uninstallStr, '/SILENT /NORESTART /SUPPRESSMSGBOXES /LOG', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0);
if not Result then
begin
Log('application uninstall failed!');
Exit
end;
Log('application uninstalled!');
// 2) Be sure to wait a while, until the actual uninstaller is deleted!
Log('waiting a while until uninstaller changes are flushed in the filesystem...');
delayCounter := 0;
repeat
Sleep(DELAY_MILLIS);
delayCounter := delayCounter + DELAY_MILLIS;
until not FileExists(uninstallStr) or (delayCounter >= MAX_DELAY_MILLIS);
if (delayCounter >= MAX_DELAY_MILLIS) then
RaiseException('Timeout exceeded trying to delete uninstaller: ' + uninstallStr);
Log('waited ' + IntToStr(delayCounter) + ' milliseconds');
end;
//============================================================================================================
// SUMMARY
// You can use this event function to perform your own pre-install and post-install tasks.
procedure CurStepChanged(CurStep: TSetupStep);
begin
if (CurStep = ssInstall) then
begin
ForceUninstallApplication();
end ;
if (CurStep = ssPostInstall) then
begin
DeleteFile(tempUninstallFilename);
end;
end;

How to avoid double-path on PATH environment variable? Using Inno-Setup [duplicate]

Inno Setup lets you set environment variables via the [Registry] sections (by setting registry key which correspond to environment variable)
However, sometimes you don't just wanna set an environment variable. Often, you wanna modify it. For example: upon installation, one may want to add/remove a directory to/from the PATH environment variable.
How can I modify the PATH environment variable from within InnoSetup?
The path in the registry key you gave is a value of type REG_EXPAND_SZ. As the Inno Setup documentation for the [Registry] section states there is a way to append elements to those:
On a string, expandsz, or multisz type value, you may use a special constant called {olddata} in this parameter. {olddata} is replaced with the previous data of the registry value. The {olddata} constant can be useful if you need to append a string to an existing value, for example, {olddata};{app}. If the value does not exist or the existing value isn't a string type, the {olddata} constant is silently removed.
So to append to the path a registry section similar to this may be used:
[Registry]
Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; \
ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};C:\foo"
which would append the "C:\foo" directory to the path.
Unfortunately this would be repeated when you install a second time, which should be fixed as well. A Check parameter with a function coded in Pascal script can be used to check whether the path does indeed need to be expanded:
[Registry]
Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; \
ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};C:\foo"; \
Check: NeedsAddPath('C:\foo')
This function reads the original path value and checks whether the given directory is already contained in it. To do so it prepends and appends semicolon chars which are used to separate directories in the path. To account for the fact that the searched for directory may be the first or last element semicolon chars are prepended and appended to the original value as well:
[Code]
function NeedsAddPath(Param: string): boolean;
var
OrigPath: string;
begin
if not RegQueryStringValue(HKEY_LOCAL_MACHINE,
'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;
Note that you may need to expand constants before you pass them as parameter to the check function, see the documentation for details.
Removing this directory from the path during uninstallation can be done in a similar fashion and is left as an exercise for the reader.
I had the same problem but despite the answers above I've ended up with a custom solution and I'd like to share it with you.
First of all I've created the environment.iss file with 2 methods - one for adding path to the environment's Path variable and second to remove it:
[Code]
const EnvironmentKey = 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment';
procedure EnvAddPath(Path: string);
var
Paths: string;
begin
{ Retrieve current path (use empty string if entry not exists) }
if not RegQueryStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths)
then Paths := '';
{ Skip if string already found in path }
if Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';') > 0 then exit;
{ App string to the end of the path variable }
Paths := Paths + ';'+ Path +';'
{ Overwrite (or create if missing) path environment variable }
if RegWriteStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths)
then Log(Format('The [%s] added to PATH: [%s]', [Path, Paths]))
else Log(Format('Error while adding the [%s] to PATH: [%s]', [Path, Paths]));
end;
procedure EnvRemovePath(Path: string);
var
Paths: string;
P: Integer;
begin
{ Skip if registry entry not exists }
if not RegQueryStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths) then
exit;
{ Skip if string not found in path }
P := Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';');
if P = 0 then exit;
{ Update path variable }
Delete(Paths, P - 1, Length(Path) + 1);
{ Overwrite path environment variable }
if RegWriteStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths)
then Log(Format('The [%s] removed from PATH: [%s]', [Path, Paths]))
else Log(Format('Error while removing the [%s] from PATH: [%s]', [Path, Paths]));
end;
Reference: RegQueryStringValue, RegWriteStringValue
Now in main .iss file I could include this file and listen for the 2 events (more about events you can learn in Event Functions section in documentation), CurStepChanged to add path after installation and CurUninstallStepChanged to remove it when user uninstall an application. In below example script add/remove the bin directory (relative to the installation directory):
#include "environment.iss"
[Setup]
ChangesEnvironment=true
; More options in setup section as well as other sections like Files, Components, Tasks...
[Code]
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssPostInstall
then EnvAddPath(ExpandConstant('{app}') +'\bin');
end;
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
begin
if CurUninstallStep = usPostUninstall
then EnvRemovePath(ExpandConstant('{app}') +'\bin');
end;
Reference: ExpandConstant
Note #1: Install step add path only once (ensures repeatability of the installation).
Note #2: Uninstall step remove only one occurrence of the path from variable.
Bonus: Installation step with checkbox "Add to PATH variable".
To add installation step with checkbox "Add to PATH variable" define new task in [Tasks] section (checked by default):
[Tasks]
Name: envPath; Description: "Add to PATH variable"
Then you can check it in CurStepChanged event:
procedure CurStepChanged(CurStep: TSetupStep);
begin
if (CurStep = ssPostInstall) and IsTaskSelected('envPath')
then EnvAddPath(ExpandConstant('{app}') +'\bin');
end;
You can use LegRoom.net's modpath.iss script in your InnoSetup script file:
#define MyTitleName "MyApp"
[Setup]
ChangesEnvironment=yes
[CustomMessages]
AppAddPath=Add application directory to your environmental path (required)
[Files]
Source: "install\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs;
[Icons]
Name: "{group}\{cm:UninstallProgram,{#MyTitleName}}"; Filename: "{uninstallexe}"; Comment: "Uninstalls {#MyTitleName}"
Name: "{group}\{#MyTitleName}"; Filename: "{app}\{#MyTitleName}.EXE"; WorkingDir: "{app}"; AppUserModelID: "{#MyTitleName}"; Comment: "Runs {#MyTitleName}"
Name: "{commondesktop}\{#MyTitleName}"; Filename: "{app}\{#MyTitleName}.EXE"; WorkingDir: "{app}"; AppUserModelID: "{#MyTitleName}"; Comment: "Runs {#MyTitleName}"
[Registry]
Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}"
[Tasks]
Name: modifypath; Description:{cm:AppAddPath};
[Code]
const
ModPathName = 'modifypath';
ModPathType = 'system';
function ModPathDir(): TArrayOfString;
begin
setArrayLength(Result, 1)
Result[0] := ExpandConstant('{app}');
end;
#include "modpath.iss"
The NeedsAddPath in the answer by #mghie doesn't check trailing \ and letter case. Fix it.
function NeedsAddPath(Param: string): boolean;
var
OrigPath: string;
begin
if not RegQueryStringValue(
HKEY_LOCAL_MACHINE,
'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(';' + UpperCase(Param) + ';', ';' + UpperCase(OrigPath) + ';') = 0) and
(Pos(';' + UpperCase(Param) + '\;', ';' + UpperCase(OrigPath) + ';') = 0);
end;
I want to thank everyone for their contributions to this question. I've incorporated about 95% of the code posted by Wojciech Mleczek into my app's installer. I do have some corrections to that code that may prove useful to others. My changes:
Renamed formal argument Path to instlPath. Cuts down on multiple uses of "Path" in code (easier to read, IMO).
When installing/uninstalling, add an existence check for an instlPath that ends with \;.
During installation, don't double up ; in the current %PATH%.
Handle missing or empty %PATH% during installation.
During uninstall, make sure that a starting index of 0 is not passed to Delete().
Here's my updated version of EnvAddPath():
const EnvironmentKey = 'Environment';
procedure EnvAddPath(instlPath: string);
var
Paths: string;
begin
{ Retrieve current path (use empty string if entry not exists) }
if not RegQueryStringValue(HKEY_CURRENT_USER, EnvironmentKey, 'Path', Paths) then
Paths := '';
if Paths = '' then
Paths := instlPath + ';'
else
begin
{ Skip if string already found in path }
if Pos(';' + Uppercase(instlPath) + ';', ';' + Uppercase(Paths) + ';') > 0 then exit;
if Pos(';' + Uppercase(instlPath) + '\;', ';' + Uppercase(Paths) + ';') > 0 then exit;
{ Append App Install Path to the end of the path variable }
Log(Format('Right(Paths, 1): [%s]', [Paths[length(Paths)]]));
if Paths[length(Paths)] = ';' then
Paths := Paths + instlPath + ';' { don't double up ';' in env(PATH) }
else
Paths := Paths + ';' + instlPath + ';' ;
end;
{ Overwrite (or create if missing) path environment variable }
if RegWriteStringValue(HKEY_CURRENT_USER, EnvironmentKey, 'Path', Paths)
then Log(Format('The [%s] added to PATH: [%s]', [instlPath, Paths]))
else Log(Format('Error while adding the [%s] to PATH: [%s]', [instlPath, Paths]));
end;
And an updated version of EnvRemovePath():
procedure EnvRemovePath(instlPath: string);
var
Paths: string;
P, Offset, DelimLen: Integer;
begin
{ Skip if registry entry not exists }
if not RegQueryStringValue(HKEY_CURRENT_USER, EnvironmentKey, 'Path', Paths) then
exit;
{ Skip if string not found in path }
DelimLen := 1; { Length(';') }
P := Pos(';' + Uppercase(instlPath) + ';', ';' + Uppercase(Paths) + ';');
if P = 0 then
begin
{ perhaps instlPath lives in Paths, but terminated by '\;' }
DelimLen := 2; { Length('\;') }
P := Pos(';' + Uppercase(instlPath) + '\;', ';' + Uppercase(Paths) + ';');
if P = 0 then exit;
end;
{ Decide where to start string subset in Delete() operation. }
if P = 1 then
Offset := 0
else
Offset := 1;
{ Update path variable }
Delete(Paths, P - Offset, Length(instlPath) + DelimLen);
{ Overwrite path environment variable }
if RegWriteStringValue(HKEY_CURRENT_USER, EnvironmentKey, 'Path', Paths)
then Log(Format('The [%s] removed from PATH: [%s]', [instlPath, Paths]))
else Log(Format('Error while removing the [%s] from PATH: [%s]', [instlPath, Paths]));
end;
Here is a complete solution to the problem that ignores casing, checks for existence of path ending with \ and also expands the constants in the param:
function NeedsAddPath(Param: string): boolean;
var
OrigPath: string;
ParamExpanded: string;
begin
//expand the setup constants like {app} from Param
ParamExpanded := ExpandConstant(Param);
if not RegQueryStringValue(HKEY_LOCAL_MACHINE,
'SYSTEM\CurrentControlSet\Control\Session Manager\Environment',
'Path', OrigPath)
then begin
Result := True;
exit;
end;
// look for the path with leading and trailing semicolon and with or without \ ending
// Pos() returns 0 if not found
Result := Pos(';' + UpperCase(ParamExpanded) + ';', ';' + UpperCase(OrigPath) + ';') = 0;
if Result = True then
Result := Pos(';' + UpperCase(ParamExpanded) + '\;', ';' + UpperCase(OrigPath) + ';') = 0;
end;
If you are ok with using an external DLL, PathMgr.dll could also be an option.
There is a sample .iss script that demonstrates how to use the DLL in Inno Setup 6 or later.
PathMgr.dll is covered by the LPGL license.

Resources