I'm creating a MSI installer and need to add more than one entry to the PATH
environment variable. Per the MSDN documentation:
Each row can contain only one value. For example, the entry
Value;Value;[~]
is more than one value and should not be used because it causes unpredictable results. The entryValue;[~]
is just one value.
My installer source code looks like this currently (note, this is a per-machine installation), which is a violation of the above documentation:
<!-- NOTE: These two features are mutually exclusive -->
<Feature Id="Feature1" Level="1000" Absent="allow" AllowAdvertise="no" InstallDefault="local" TypicalDefault="install">
<Component Directory="INSTALLFOLDER">
<RegistryValue Action="write" Type="int" Root="HKLM" Key="SOFTWARE\MyProduct" Name="MyPathEntry1" Value="1" KeyPath="yes" />
<Environment Id="AddPathEntry1" Name="PATH" Value="[INSTALLFOLDER]SubDir1" Action="set" Permanent="yes" Part="last" System="yes" />
</Component>
</Feature>
<Feature Id="Feature2" Level="1000" Absent="allow" AllowAdvertise="no" InstallDefault="local" TypicalDefault="install">
<Component Directory="INSTALLFOLDER">
<RegistryValue Action="write" Type="int" Root="HKLM" Key="SOFTWARE\MyProduct" Name="MyPathEntry2" Value="1" KeyPath="yes" />
<Environment Id="AddPathEntry2" Name="PATH" Value="[INSTALLFOLDER]SubDir1;[INSTALLFOLDER]SubDir2;[INSTALLFOLDER]SubDir3" Action="set" Permanent="yes" Part="last" System="yes" />
</Component>
</Feature>
Now even though the above is "technically" a violation according to the MSDN documentation, it seems to work. I've tested fresh installations, modifying an installation, and upgrading. All seem to work with no hitch. But one thing I've learned with MSI is whenever possible, it's best to follow the rules to avoid messing up people's machines.
The natural solution of adding independent (i.e. not mutually exclusive) features containing only individual path components won't work because with MSI, you cannot guarantee the order in which features and/or components are installed. However, in this case, the order in which path components are added to the PATH
environment variable is important due to how the PATH
variable is used when finding unqualified executables.
The other question that may come to mind is, why am I using features? I want to give installers of the product the option to change their installation choice at a later date in time via the standard Add/Remove Programs or Program and Features Control Panel applet.
So, how can I add more than one path entry to the PATH
environment variable in a deterministic order while following the recommended guidance from the MSDN? Or is the guidance from MSDN outdated and what I'm currently doing is perfectly fine?
I figured a way to do this which does not violate the guidance at MSDN for my particular situation.
The exact situation is I have a main program executable that installers may or may not want to be able to run from a Windows Command Shell. Additionally, there are other utilities that also installers may or may not wish to run from a Windows Command Shell.
The way this was originally modeled was as a set of 3 mutually exclusive features using radio buttons to decide how to update the system PATH
environment variable. The 3 features boiled down to:
PATH
environment variable (i.e. the program and optional tools can't be run from a Windows Command Shell by simply typing the name of the executable at any directory location).PATH
environment variable so that only the main executable can be run from a Windows Command Shell at any given path.PATH
environment variable to also add additional utility tool paths.What I came to realize is that there are really only 2 features here, and they're not even mutually exclusive, but one is dependent on the other. The first radio button listed above is not really a feature--it's a no-op; it doesn't install anything! The second radio button represents the "main" feature in that the primary executable's directory is added to the system's PATH
. The 3rd radio button depends upon the second: if the main executable is added to the PATH
, then you can additionally add other utilities to the PATH
as well.
This results in a much simpler UI. Instead of radio buttons, use checkboxes, and the second checkbox is disabled unless the first checkbox is checked. This also results in a much simpler implementaion for ensuring that the combination of features selected (say, for a command-line installation) is valid.
In a nutshell, here's what the WiX XML will boil down to in my particular case. I'm sure that this can be generalized (or modified) to suit other similar scenarios.
<Feature Id="AddExeToPath" Level="1" Absent="allow" AllowAdvertise="no" InstallDefault="local" TypicalDefault="install">
<Component Id="ExePath" Guid="{YOURGUID-HERE-0000-0000-00000000}" Directory="INSTALLFOLDER">
<CreateFolder />
<Environment Id="ExePath" Name="MY_PATHS" Value="[INSTALLFOLDER]SubDir1" Action="set" Part="first" System="yes" />
</Component>
<Component Directory="INSTALLFOLDER">
<RegistryValue Action="write" Type="integer" Root="HKLM" Key="SOFTWARE\MyProduct" Name="AddExeToPath" Value="1" KeyPath="yes" />
<Environment Id="AddToPath" Name="PATH" Value="%MY_PATHS%" Action="set" Part="last" System="yes" />
</Component>
<Component Id="RemoveExeFromPathOnUninstall" Guid="{YOURGUID-HERE-0000-0000-00000000}" Directory="INSTALLFOLDER">
<Condition>REMOVE><AddExeToPath OR REMOVE="ALL"</Condition>
<Environment Id="RemoveFromPath" Name="PATH" Value="%MY_PATHS%" Action="remove" />
</Component>
</Feature>
<Feature Id="AddToolsToPath" Level="1000" Absent="allow" AllowAdvertise="no" InstallDefaut="local" TypicalDefault="install">
<Component Id="SubDir2" Guid="{YOURGUID-HERE-0000-0000-00000000}" Directory="INSTALLFOLDER">
<CreateFolder />
<Environment Id="SubDir2" Name="MY_TOOLS" Value="[INSTALLFOLDER]SubDir2" Action="set" Part="first" System="yes" />
</Component>
<Component Id="SubDir3" Guid="{YOURGUID-HERE-0000-0000-00000000}" Directory="INSTALLFOLDER">
<CreateFolder />
<Environment Id="SubDir3" Name="MY_TOOLS" Value="[INSTALLFOLDER]SubDir3" Action="set" Part="last" System="yes" />
</Component>
<Component Directory="INSTALLFOLDER">
<RegistryValue Action="write" Type="integer" Root="HKLM" Key="SOFTWARE\MyProduct" Name="AddToolsToPath" Value="1" KeyPath="yes" />
<Environment Id="AddToolsToPath" Name="MY_PATHS" Value="%MY_TOOLS%" Action="set" Part="last" System="yes" />
</Component>
</Feature>
<CustomAction Id="InvalidPathFeatureSelection" Error="25000" Execute="firstSequence" />
<InstallExecuteSequence>
<Custom Action="InvalidPathFeatureSelection" Before="InstallValidate">
<![CDATA[NOT (REMOVE="ALL" OR (&AddToolsToPath >= 3 IMP &AddExeToPath >= 3))]]>
</Custom>
</InstallExecuteSequence>
This set of features and components results in the following:
If the feature AddExeToPath is selected for installation, the following components of the feature result in:
MY_PATHS
is created and contains the path to the main executable.PATH
is updated, placing %MY_PATHS%
at the end its current value.If the feature AddToolsToPath is selected, 3 components are installed:
MY_TOOLS
is created/set, and [INSTALLFOLDER]SubDir2
is put at the front of the variable's existing value (if any).MY_TOOLS
is created/set and [INSTALLFOLDER]SubDir3
is put at the end of the variable's existing value (if any).Note
These two components defined the way that they are ensure that the paths added toMY_TOOLS
are added in the appropriate order.
MY_PATHS
is updated, placing %MY_TOOLS%
at the end of the exisiting variable; again, this preserves the correct ordering of paths.And so what you end up with is:
MY_TOOLS
= [INSTALLFOLDER]SubDir2;[INSTALLFOLDER]SubDir3
MY_PATHS
= [INSTALLFOLDER]SubDir1;%MY_TOOLS%
PATH
= <existing_PATH_value>;%MY_PATHS%