Related
I am trying to make a application which is based around backing up data. I would like to add a option to copy a folder with its contents while ignoring the subfolders only.
I use TDirectory.Copy('C:\folder','C:\folder2'); to copy folders but that has no additional data requested other than the directory to copy and where to copy.
So, Is there a simple way to achieve this?
A function that can be called would work too.
It's not perfect, but you could make your own routine based on this :
procedure TForm2.Button1Click(Sender: TObject);
var
aSourceDir : String;
aDestDir : String;
aFileList : TStringDynArray;
iFile : Integer;
aSourceFileName : String;
aDestFileName : String;
begin
aSourceDir := 'C:\DEV\GitRepositories\TestProjects\WithStatementSample\';
aDestDir := 'C:\DEV\GitRepositories\TestProjects\WithStatementSample2\';
aFileList := TDirectory.GetFiles( aSourceDir );
if not ( TDirectory.Exists( aDestDir ) ) and
( Length( aFileList ) > 0 ) then
begin
TDirectory.CreateDirectory( aDestDir );
end;
for iFile := 0 to Pred( Length( aFileList ) ) do
begin
aSourceFileName := aFileList[ iFile ];
aDestFileName := IncludeTrailingPathDelimiter( aDestDir ) +
ExtractFileName( aFileList[ iFile ] );
TFile.Copy( aSourceFileName, aDestFileName );
end;
end;
This will simply loop over every file found in the Source directory and copy it to the destination directory. It will not copy folders, nor copy contents of the folders.
Again, it's not 100% fool/bullet proof though, so you will have to adapt it to your needs if necessary.
Dec.26,2022 Update... The example code I submitted used literal values which
actually worked but not for my application. so...the problem with vars was
PAnsiChar not managing strings, so... (rather verbose code)... sSCBHostPath :=
gsAppPath + 'SCBHosts\'; sDirName := 'SCBHost' +
Trim(absqMyGuests.FieldByName('Guest_Name').AsString); sNewHostAppPath :=
sSCBHostPath + sDirName; sPCharString := 'XCopy ' + sAppDeploymentPath + ' ' +
sNewHostAppPath + ' /E /H /C /I'; cPChar {PAnsiChar type} :=
StringToPAnsiChar(sPCharString); //to reader, function code follows TRY
WinExec(cPChar,SW_SHOWNORMAL); EXCEPT ON E:EXCEPTION DO messagedlg('Exception:
' + e.Message,mterror,[mbok],0); END; // This function converts a string to a
PAnsiChar // If the output is not the same, an exception is raised // Author:
nogabel#hotmail.com function
TfInternetServersAndClients.StringToPAnsiChar(stringVar : string) : PAnsiChar;
Var AnsString : AnsiString; InternalError : Boolean; begin InternalError :=
false; Result := ''; try if stringVar <> '' Then begin AnsString :=
AnsiString(StringVar); Result := PAnsiChar(PAnsiString(AnsString)); end;
Except InternalError := true; end; if InternalError or (String(Result) <>
stringVar) then begin Raise Exception.Create('Conversion from string to
PAnsiChar failed!'); end; end; Dec.25,2022 Delphi XE2 Pro...
This opens Command.com and copies an entire folder and its subfolders and all files to a different drive, then disappears.
WinExec(PAnsiChar('XCopy C:\Dir1 E:\Dir1 /E /H /C /I'),SW_SHOWNORMAL);
Next.
This opens Command.com and copies an entire folder and its subfolders and all files to an existing folder on the same drive named C:\NewDir. The resulting C:\NewDir will contain a folder named Dir1 with all of its subfolders and files.
WinExec(PAnsiChar('XCopy C:\Dir1 C:\NewDir /E /H /C /I'),SW_SHOWNORMAL);
I'm using this code:
str := ExtractFilePath(ParamStr(0)) + '\Connection.ini';
to get the path of an .ini file, but I want to go up by 2 directories, so the .ini file doesn't sit in the DEBUG folder.
I tried this:
str := ExtractFilePath(ParamStr(0)) + '\..\..\Connection.ini';
But didn't work...
There are quite a few possibilities to go one directory up. Some of them include:
str := ExtractFilePath(ExtractFilePath(ParamStr(0))) + '\Connection.ini';
or
str := IncludeTrailingPathDelimiter(ExtractFilePath(ExtractFilePath(ParamStr(0)))) + 'Connection.ini';
or
str := ExtractFilePath(ParamStr(0)) + '\..\Connection.ini';
I use this function:
TYPE DirStr = STRING;
TYPE CpuWord = Cardinal;
FUNCTION EXECPATH : DirStr;
BEGIN
Result:=IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)))
END;
FUNCTION XPATH : DirStr;
VAR
P : CpuWord;
BEGIN
Result:=EXECPATH;
{$IFDEF WINDOWS }
{$IFDEF CPU64BITS }
P:=POS('\WIN64\',UpperCase(Result));
{$ELSE }
P:=POS('\WIN32\',UpperCase(Result));
{$ENDIF }
IF P>0 THEN SetLength(Result,P)
{$ENDIF }
END;
That way, it'll work both when the file is within the default directory when compiling from the IDE and when run from an installation directory.
Use EXECPATH if you want the true directory that the .EXE file resides in, and XPATH is you want the "logical" directory (ie. if within \Win32\Debug and the like, step out of this).
Adapting the answer from Ondrej, I used this method:
function GetParentFolder(folder: String; const cLevels: BYTE = 1) : String;
var
parent: String;
level: BYTE;
begin
// Given "C:\Parent\Child\" or "C:\Parent\Child\MyFile.txt", return "C:\Parent\"
if (cLevels > 0) then
begin
parent := ExcludeTrailingPathDelimiter(folder);
level := 0;
while (level < cLevels) do
begin
parent := ExcludeTrailingPathDelimiter(ExtractFileDir(ExtractFilePath(parent)));
Inc(level);
end;
Result := IncludeTrailingPathDelimiter(parent);
end
else
Result := folder;
end;
And use it like this:
str := GetParentFolder(ParamStr(0), 2) + 'Connection.ini';
The advantage, IMO, is that this works with both a full path or just a directory (without filename). Moreover, you can navigate any number of levels up as required.
As I am looking for AES-128 encryption, I'd like to get Lockbox3 running on Delphi2010.
The first problem here: What/where are the official sources?
The sources from https://sourceforge.net/projects/tplockbox/ don't hold packages for Delphi2010 and also simply don't compile (loads of errors).
https://code.google.com/archive/p/tplockbox/ is not maintained anymore and points to https://github.com/SeanBDurkin/tplockbox.
I downloaded the sources from github, I think in V3.6.3 (version is nowhere mentioned in the sources, right?). The packages can be installed, but e.g. the MakeSampleKey example doesn't compile, as EncryptString doesn't work with AnsiStrings (umfmMakeSampleKey.pas, line 216).
I have then created a project and used the source from the OP of How to AES-128 encrypt a string using a password in Delphi and decrypt in C#?
I changed CipherText from AnsiString to String. The code compiles, but when I run it, it crashes with "Integer overflow" in TPLB3.SHA1.pas, line 264.
Is LockBox3 still maintained and is it usable for Delphi2010? If yes, then how? What do I do wrong? Thx!
Edit: There's another GitHub project hosting LockBox3, namely https://github.com/TurboPack/LockBox3
The recent sources from there do NOT compile under Delphi2010. (see comments under OP for a short list of problems)
Edit: Here's some code I try to use (and fail) - i post it here as I don't manage to post it formatted into a comment:
function LockBox3_EncryptText_AES_128(input: string; password: string): string;
var
Codec: TCodec;
CipherText: String;
begin
Codec := TCodec.Create(nil);
try
Codec.CryptoLibrary := TCryptographicLibrary.Create(Codec);
Codec.StreamCipherId := BlockCipher_ProgID;
Codec.BlockCipherId := Format(AES_ProgId, [128]);
Codec.ChainModeId := CBC_ProgId;
Codec.Password := Password;
Codec.EncryptString(input, CipherText);
Result := string(CipherText);
finally
Codec.Free;
end;
end;
I maintain LockBox 3 at http://lockbox.seanbdurkin.id.au/HomePage .
The repo is at https://github.com/SeanBDurkin/tplockbox .
Yes, it works for D2010.
Update
This works for me, with Delphi 2010 and TPLB3 version 3.6.3
program LB3Demo_D2010;
{$APPTYPE CONSOLE}
uses
SysUtils,
TPLB3.Codec in '..\ExternalLibraries\TPLB3\run\TPLB3.Codec.pas',
TPLB3.CryptographicLibrary in '..\ExternalLibraries\TPLB3\run\TPLB3.CryptographicLibrary.pas',
TPLB3.BlockCipher in '..\ExternalLibraries\TPLB3\run\TPLB3.BlockCipher.pas',
TPLB3.StreamToBlock in '..\ExternalLibraries\TPLB3\run\TPLB3.StreamToBlock.pas',
TPLB3.Decorators in '..\ExternalLibraries\TPLB3\run\TPLB3.Decorators.pas',
TPLB3.StreamCipher in '..\ExternalLibraries\TPLB3\run\TPLB3.StreamCipher.pas',
TPLB3.StreamUtils in '..\ExternalLibraries\TPLB3\run\TPLB3.StreamUtils.pas',
TPLB3.Random in '..\ExternalLibraries\TPLB3\run\TPLB3.Random.pas',
TPLB3.IntegerUtils in '..\ExternalLibraries\TPLB3\run\TPLB3.IntegerUtils.pas',
TPLB3.Compatibility in '..\ExternalLibraries\TPLB3\run\TPLB3.Compatibility.pas',
TPLB3.Asymetric in '..\ExternalLibraries\TPLB3\run\TPLB3.Asymetric.pas',
TPLB3.CodecIntf in '..\ExternalLibraries\TPLB3\run\TPLB3.CodecIntf.pas',
TPLB3.BaseNonVisualComponent in '..\ExternalLibraries\TPLB3\run\TPLB3.BaseNonVisualComponent.pas',
TPLB3.Hash in '..\ExternalLibraries\TPLB3\run\TPLB3.Hash.pas',
TPLB3.HashDsc in '..\ExternalLibraries\TPLB3\run\TPLB3.HashDsc.pas',
TPLB3.AES in '..\ExternalLibraries\TPLB3\run\TPLB3.AES.pas',
TPLB3.Base64 in '..\ExternalLibraries\TPLB3\run\TPLB3.Base64.pas',
TPLB3.CBC in '..\ExternalLibraries\TPLB3\run\TPLB3.CBC.pas',
TPLB3.Constants in '..\ExternalLibraries\TPLB3\run\TPLB3.Constants.pas',
TPLB3.ECB in '..\ExternalLibraries\TPLB3\run\TPLB3.ECB.pas',
TPLB3.MD5 in '..\ExternalLibraries\TPLB3\run\TPLB3.MD5.pas',
TPLB3.SimpleBlockCipher in '..\ExternalLibraries\TPLB3\run\TPLB3.SimpleBlockCipher.pas',
TPLB3.I18n in '..\ExternalLibraries\TPLB3\run\TPLB3.I18n.pas',
TPLB3.CFB_8Bit in '..\ExternalLibraries\TPLB3\run\TPLB3.CFB_8Bit.pas',
TPLB3.CFB_Block in '..\ExternalLibraries\TPLB3\run\TPLB3.CFB_Block.pas',
TPLB3.CTR in '..\ExternalLibraries\TPLB3\run\TPLB3.CTR.pas',
TPLB3.OFB in '..\ExternalLibraries\TPLB3\run\TPLB3.OFB.pas',
TPLB3.PCBC in '..\ExternalLibraries\TPLB3\run\TPLB3.PCBC.pas',
TPLB3.SHA1 in '..\ExternalLibraries\TPLB3\run\TPLB3.SHA1.pas',
TPLB3.SHA2 in '..\ExternalLibraries\TPLB3\run\TPLB3.SHA2.pas',
TPLB3.SVN_Keywords in '..\ExternalLibraries\TPLB3\run\TPLB3.SVN_Keywords.pas',
TPLB3.BinaryUtils in '..\ExternalLibraries\TPLB3\run\TPLB3.BinaryUtils.pas',
TPLB3.PointerArithmetic in '..\ExternalLibraries\TPLB3\run\TPLB3.PointerArithmetic.pas',
TPLB3.CipherUtils in '..\ExternalLibraries\TPLB3\run\TPLB3.CipherUtils.pas',
TPLB3.RSA_Engine in '..\ExternalLibraries\TPLB3\run\TPLB3.RSA_Engine.pas',
TPLB3.RSA_Primitives in '..\ExternalLibraries\TPLB3\run\TPLB3.RSA_Primitives.pas',
TPLB3.HugeCardinal in '..\ExternalLibraries\TPLB3\run\TPLB3.HugeCardinal.pas',
TPLB3.HugeCardinalUtils in '..\ExternalLibraries\TPLB3\run\TPLB3.HugeCardinalUtils.pas',
TPLB3.MemoryStreamPool in '..\ExternalLibraries\TPLB3\run\TPLB3.MemoryStreamPool.pas',
TPLB3.DES in '..\ExternalLibraries\TPLB3\run\TPLB3.DES.pas',
TPLB3.BlowFish in '..\ExternalLibraries\TPLB3\run\TPLB3.BlowFish.pas',
TPLB3.TDES in '..\ExternalLibraries\TPLB3\run\TPLB3.TDES.pas',
TPLB3.TwoFish in '..\ExternalLibraries\TPLB3\run\TPLB3.TwoFish.pas',
TPLB3.XXTEA in '..\ExternalLibraries\TPLB3\run\TPLB3.XXTEA.pas',
TPLB3.DCP.twofish_Modified in '..\ExternalLibraries\TPLB3\run\TPLB3.DCP.twofish_Modified.pas';
const
/// <remarks>Set isProduction to True for a production environment.
/// For a production environment, we want to randomize the PRNG at start-up,
/// for security reasons. For a test environment, we may way to set the seed
/// to be a fixed known value, for purposes of reproducibility and possibly
/// KAT alignment.
/// </remarks>
isProduction: boolean = False;
Seed_ForNonProduction: int64 = 1;
function LockBox3_EncryptText_AES_128( input: string; password: string): string;
var
Codec: TCodec;
begin
Codec := TCodec.Create( nil);
try
Codec.CryptoLibrary := TCryptographicLibrary.Create(Codec);
Codec.StreamCipherId := BlockCipher_ProgID;
Codec.BlockCipherId := Format(AES_ProgId, [128]);
Codec.ChainModeId := CBC_ProgId;
Codec.Password := Password;
Codec.EncryptString( input, result);
Codec.Burn
finally
Codec.Free
end
end;
var
input, output: string;
password: string;
begin
try
if isProduction then
TRandomStream.Instance.Randomize
else
TRandomStream.Instance.Seed := Seed_ForNonProduction;
input := 'Hello world';
WriteLn( 'Compiler = ', Format( '%.1f', [CompilerVersion]));
WriteLn( 'Plaintext = "' + input + '"');
password := 'my-secret';
WriteLn( 'Password (' + {$IFDEF UNICODE} 'UTF-16' {$ELSE} 'UTF-8' {$ENDIF} + ') = "' + password + '"');
WriteLn( 'Seed = ', TRandomStream.Instance.Seed);
output := LockBox3_EncryptText_AES_128( input, password);
Writeln( 'Ciphertext (encoded as base64) = "' + output + '"');
WriteLn( 'Press enter to terminate.');
Readln;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
Output
When run, the output yields ...
Compiler = 21.0
Plaintext = "Hello world"
Password (UTF-16) = "my-secret"
Seed = 1
Ciphertext (encoded as base64) = "AQAAAAAAAADCpkdd/g8fyEuojQ=="
I have a license.exe file that I call in my setup code at the end,
The code needs the environment variable to be set before working correctly,
The code is as follows:
[Registry]
; set PATH
Root: HKLM; \
Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; \
ValueType: string; ValueName: "PATH"; ValueData: "{app}"
[Setup]
; Tell Windows Explorer to reload the environment
ChangesEnvironment=yes
[Run]
Filename: "{app}\temp\installation_files\license.exe";
Here the code executes, but does not find the correct path.
When I check the system environment variable, it is set correctly,
When I run the license.exe code afterwards manually, it works correctly and sees the environment variable.
Can someone tell me how to fix this?
Or how to delay the [Run] section until the system recognizes the environment variable?
The processes created for executing entries from the [Run] section inherits the environment block of its parent process, which is the installer itself. So you have to set the environment variable to the installer and let it inherit to your executed application. How to do that is shown in the below script:
[Run]
Filename: "{app}\temp\installation_files\license.exe"; BeforeInstall: SetEnvPath
[Code]
#ifdef UNICODE
#define AW "W"
#else
#define AW "A"
#endif
function SetEnvironmentVariable(lpName: string; lpValue: string): BOOL;
external 'SetEnvironmentVariable{#AW}#kernel32.dll stdcall';
procedure SetEnvPath;
begin
if not SetEnvironmentVariable('PATH', ExpandConstant('{app}')) then
MsgBox(SysErrorMessage(DLLGetLastError), mbError, MB_OK);
end;
Previous answer for notifying rest of the system about variable change:
As #Jerry pointed out in his comment, a notification about the environment changes is performed after the [Run] section is processed. Actually, it is one of the last things executed by the installer.
So, to notify the system about environment changes before processing the [Run] section, you'll need to have a workaround. I rewrote the RefreshEnvironment procedure from Inno Setup code to script. It's the same function as it's executed if you have ChangesEnvironment directive set to yes.
In the following script I have removed the ChangesEnvironment directive and added execution of the RefreshEnvironment procedure from the AfterInstall parameter function of your registry entry:
[Registry]
Root: HKLM; \
Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; \
ValueType: string; ValueName: "PATH"; ValueData: "{app}"; \
AfterInstall: RefreshEnvironment;
[Run]
Filename: "{app}\temp\installation_files\license.exe";
[Code]
const
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 RefreshEnvironment;
var
S: AnsiString;
MsgResult: DWORD;
begin
S := 'Environment';
SendTextMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0,
PAnsiChar(S), SMTO_ABORTIFHUNG, 5000, MsgResult);
end;
The solution with SetEnvironmentVariable in TLama's answer is correct for many situations.
But it won't work for [Run] tasks with runasoriginaluser flag (what is implied by postinstall flag). I.e. the variable won't be propagated to an application run with common "Run My Program" check box on the "Finished" page.
The reason is that the tasks with runasoriginaluser are executed by a un-elevated hidden parent process of the Inno Setup installer. The SetEnvironmentVariable will change environment for the installer, but not for its parent process. Unfortunately, the parent process of the installer cannot be controlled (imo).
As a workaround, to set the variable for the runasoriginaluser tasks, you have to inject an intermediate process between the installer parent process and the task, and have the intermediate process set the variable.
Such an intermediate process can easily be the cmd.exe with its set command:
[Run]
Filename: "{cmd}"; Parameters: "/C set PATH=%PATH%;{app} & ""{app}\MyProg.exe"""; \
Description: "Run My Program"; Flags: postinstall runhidden
The runhidden flag hides the cmd.exe console window, not the application (assuming it's a GUI application). If it's a console application and you want the output to be visible, remove the runhidden flag. Alternatively, you can use start command to start the application in its own console window.
after some modifications below, worked perfectly:
[Run]
Filename: "{app}\{#MyAppExeName}"; BeforeInstall: AppendToPathAndRefresh;Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}";Flags: nowait postinstall shellexec skipifsilent
[Code]
////////////////////////////////////////////////////////////
const
SMTO_ABORTIFHUNG = 2;
WM_WININICHANGE = $001A;
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 RefreshEnvironment;
var
S: AnsiString;
MsgResult: DWORD;
begin
S := 'Environment';
SendTextMessageTimeout(HWND_BROADCAST, WM_WININICHANGE, 0,
PAnsiChar(S), SMTO_ABORTIFHUNG, 5000, MsgResult);
end;
///PATH ENVINRONMENT//////////////////////////////////////////
function Replace(Dest, SubStr, Str: string): string;
var
Position: Integer;
Ok: Integer;
begin
Ok := 1;
while Ok > 0 do
begin
Position:=Pos(SubStr, Dest);
if Position > 0 then
begin
Delete(Dest, Position, Length(SubStr));
Insert(Str, Dest, Position);
end else
Ok := 0;
end;
Result:=Dest;
end;
procedure AppendToPath();
var
V: string;
Str: string;
begin
RegQueryStringValue(HKLM, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'Path', V)
Str := ExpandConstant('{app}\libav');
V := Replace(V, Str, '');
V := V + ';' + Str;
V := Replace(V,';;',';');
RegWriteStringValue(HKLM, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'Path', V)
// MsgBox(V, mbInformation, MB_OK);
end;
procedure RemoveFromPath();
var
V: string;
Str: string;
begin
RegQueryStringValue(HKLM, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'Path', V)
Str := ExpandConstant('{app}\dlls');
V := Replace(V, Str, '');
V := Replace(V,';;',';');
RegWriteStringValue(HKLM, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'Path', V)
//MsgBox(V, mbInformation, MB_OK);
end;
procedure AppendToPathAndRefresh;
begin
AppendToPath;
RefreshEnvironment;
end;
procedure DeinitializeUninstall();
begin
RemoveFromPath();
end;
///END OF PATH ENVIRONMENT ///////////////////////////////////
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.