bandana
bandana

Reputation: 3612

How do I modify the PATH environment variable when running an Inno Setup Installer?

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?

Upvotes: 74

Views: 58590

Answers (9)

Bill_Stewart
Bill_Stewart

Reputation: 24585

If you are ok with using an external DLL or executable, PathMgr might be the best option. It offers the following features:

  • Supports both current user and system path
  • Ability to check if a directory is or isn't in the path already
  • Don't add directory if already exists in path
  • Accounts for environment variable string expansion in path directories
  • Supports directory names containing the ; character

I don't think the other solutions here account for all of these details--I believe PathMgr is the most robust solution by far.

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.

Upvotes: 0

Wernfried Domscheit
Wernfried Domscheit

Reputation: 59622

My version is this one. I don't like when a setup puts a folder at first place in the PATH (maybe developers think, "my tool is the most important one"), so you can define the position.

[Tasks]
Name: envPath; Description: "Add '{app}' to PATH variable" 

[Code]
const EnvironmentKey = 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment';

procedure CurStepChanged(CurStep: TSetupStep);
var
  currentPath: string;
  newPath: string;
  folder: string;
  f: integer;
  len: integer;
  added: boolean;  
begin
  if (CurStep = ssPostInstall) and IsTaskSelected('envPath') then
  begin  
    RegQueryStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', currentPath);
    Log('Current %PATH% = ' + currentPath);

    // Check if folder is already added to PATH
    if (Pos(';' + Uppercase(ExpandConstant('{app}')) + ';', ';' + Uppercase(currentPath) + ';') > 0) or
      (Pos(';' + Uppercase(AddBackslash(ExpandConstant('{app}'))) + ';', ';' + Uppercase(currentPath) + ';') > 0) then
    begin
      Log('Folder "' + ExpandConstant('{app}') + '" already added to %PATH%');
      exit;
    end;

    // Add folder to PATH at 4th position
    f := 1;
    added := false;   
    RegQueryStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', currentPath);
    currentPath := currentPath + ';'
    Log('Adding "' + ExpandConstant('{app}') + '" to %PATH%');
    repeat    
      len := Pos(';', Copy(currentPath, 1, Pos(';', currentPath)));
      folder := Copy(currentPath, 1, len);
      newPath := newPath + folder;
      if f = 4 then
      begin
        newPath := newPath + ExpandConstant('{app}') + ';';
        added := true;
      end;
      currentPath := Copy(currentPath, len+1, Length(currentPath));
      f := f + 1;
    until Length(folder) = 0;

    // If PATH has less than 4 entries, append folder at the end. Otherwise remove trailing ";"
    if not added then
      newPath := newPath + ExpandConstant('{app}')
    else 
      newPath := Copy(newPath, 1, Length(newPath)-1);

    Log('New %PATH% = ' + newPath);
    RegWriteStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', newPath);
  end;
end;


procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
var
  currentPath: string;
  newPath: string;
  folder: string;
  f: integer;
  len: integer;
begin
  if CurUninstallStep = usPostUninstall then
  begin
    RegQueryStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', currentPath);
    Log('Current %PATH% = ' + currentPath);

    // Check if folder exist in PATH
    if (Pos(';' + Uppercase(ExpandConstant('{app}')) + ';', ';' + Uppercase(currentPath) + ';') = 0) and
      (Pos(';' + Uppercase(AddBackslash(ExpandConstant('{app}'))) + ';', ';' + Uppercase(currentPath) + ';') = 0) then 
    begin
      Log('Folder "' + ExpandConstant('{app}') + '" not found in %PATH%');
      exit;
    end;

    currentPath := currentPath + ';';    
    repeat    
      len := Pos(';', Copy(currentPath, 1, Pos(';', currentPath)));
      folder := Copy(currentPath, 1, len-1);
      if Length(folder) = 0 then break;
      if Uppercase(ExpandConstant('{app}')) <> Uppercase(RemoveBackslash(folder)) then
        newPath := newPath + folder + ';'
      else
        Log('Removing folder "' + ExpandConstant('{app}') + '" from %PATH%');
      currentPath := Copy(currentPath, len+1, Length(currentPath));
    until false;
    newPath := Copy(newPath, 1, Length(newPath)-1);

    Log('New %PATH% = ' + newPath);
    RegWriteStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', newPath);
  end;
end;

Usually you should run a few more checks, for example whether the registry key exists or whether it contains already a value. However, I think for PATH variable this is not needed, because such errors are very unlikely.

Note, Registry key HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment\Path shows they system PATH variable. The user PATH value is stored in HKCU\Environment\Path. In some cases function GetEnv('PATH') might be more useful to retrieve current PATH value.

Note, GetEnv('PATH') resolves variables (e.g. %SYSTEMROOT% to C:\WINDOWS) whereas from the registry you get the variables.

Upvotes: 0

Lovish Garg
Lovish Garg

Reputation: 26

I just added this and it worked:

[Setup]
ChangesEnvironment=yes

[Registry]
Root: HKCU; Subkey: "Environment"; ValueType:string; ValueName: "Path"; ValueData: "{olddata};{app}";

Upvotes: 1

decaffeinated
decaffeinated

Reputation: 86

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;

Upvotes: 6

Wojciech Mleczek
Wojciech Mleczek

Reputation: 1634

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".

Inno Setup - 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;

Upvotes: 37

Helen Dyakonova
Helen Dyakonova

Reputation: 129

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;

Upvotes: 8

mghie
mghie

Reputation: 32344

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.

Upvotes: 98

vezenkov
vezenkov

Reputation: 4155

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;

Upvotes: 2

ecle
ecle

Reputation: 4000

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"

Upvotes: 18

Related Questions