How to Create Stub MSI With Wix
Have you ever run into the situation where you need to deploy an exe file but you can only push an msi. An easy solution to work around this problem is called creating a stub MSI.
A stub msi is a simple msi package that will run a command line on install and uninstall.
Setting up
Before you get started you are going to need a few things.
WIX Toolkit
The first thing you are going to need is the Wix Toolkit. This is a simple command line tool which lets you create msi files from xml source files. If you don’t understand how MSI files work or how xml works don’t worry in this blog post I will walk you through how to update a template.
To get wix you can download and install it from here or you can install it via chocolatey via:
choco install WixToolkit -y
Installer information
The next thing you are going to need to do is create a folder and put your installer exe file inside it.
The next thing you will need to do is find the command line arguments to install and uninstall the program silently.
You can generally find this information on the vendor’s website or if you google applicationname silent install you can find someone in a support forum who has posted the details.
Script files
Next up we just need to create the script files that will generate the msi.
Main.wxs
First file to create is main.wxs and copy the following into it.
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="*" UpgradeCode="GUIDHERE" Version="1.0.0.0" Language="1033" Name="MYAPPNAME" Manufacturer="Foosoft">
<Package InstallerVersion="300" Compressed="yes" Platform="x86"/>
<Media Id="1" Cabinet="data.cab" EmbedCab="yes"/>
<MajorUpgrade DowngradeErrorMessage="A later version of [ProductName] is already installed. Setup will now exit." />
<Binary Id="INSTEXE" SourceFile="myinstall.exe" />
<CustomAction Id="InstallEXE" BinaryKey="INSTEXE" Execute="deferred" Impersonate="no" ExeCommand="/S" Return="check" />
<CustomAction Id="UninstallEXE" BinaryKey="INSTEXE" Execute="deferred" Impersonate="no" ExeCommand="/U /S" Return="check" />
<InstallExecuteSequence>
<Custom After="InstallInitialize" Action="InstallEXE">NOT REMOVE</Custom>
<Custom After="InstallEXE" Action="UninstallEXE">REMOVE="ALL"</Custom>
</InstallExecuteSequence>
<Directory Id='TARGETDIR' Name='SourceDir' >
<Component Id="InstalledReg">
<RegistryKey Root="HKCU" Key="Software\Microsoft\MyApplicationName">
<RegistryValue Type="integer" Name="Installed" Value="1" KeyPath="yes"/>
</RegistryKey>
</Component>
</Directory>
<Feature Id='MainFeature' Title='Main' Level='1'>
<ComponentRef Id="InstalledReg" />
</Feature>
</Product>
</Wix>
Now in the script created above you need to replace some of the text with properties that match your application.
-
UpgradeCode - This needs to be replaced (GUIDHERE) with a new guid. This should stay the same between each version of the package but should not be shared between other applications. It is used to allow the program to upgrade. To create a new guid simply type
[Guid]::NewGuid()
into powershell (e.g. 59e6b63f-4e47-441d-8e16-9bf2783468e6). -
Version - Replace this with a version number in the format of #.#.#.# (e.g 1.2.3.4).
-
Name - Name of the application, this will appear in add remove programs.
-
Manufacturer - Company which created the application.
-
Binary - You will see the SourceFiles value pointing to an install file. You need to change this to match your installer executable.
-
CustomActions - You will see InstallExe and UninstallExe custom actions. These specify the commandline options you want to install during install and uninstall. Set ExeCommand to the parameters you want to run during install (do not include the exe in this property).
-
RegistryKey - You need to also change the registry key the package writes when it is installed. You can set this to anything that makes sense.
Build.ps1
Once this is all done you are ready to build you can create a simple build script to do the install.
Create build.ps1 file and put it in the same directory as the other scrip.t
$wixdir = "C:\Program Files (x86)\WiX Toolset v3.11\bin\"
&"$wixdir\candle.exe" *.wxs
&"$wixdir\light.exe" -out stub.msi main.wixobj
You won’t need to change much in this script but if you have installed wix into a different directory you can change $wixdir to point to where its installed.
You can also rename the msi file to anything you like instead of stub.msi.
Now you can simply run build.ps1 to generate an msi for you.
Extra actions
If you want to add extra custom actions you can do so.
Use <Binary Id="UNIQUENAME" SourceFile="myinstall.exe" />
to embed an exe in the msi. Change the UNIQUENAME to something unique.
Then add another custom action tag like <CustomAction Id="UNIQUENAME" BinaryKey="NAMEOFBIN" Execute="deferred" Impersonate="no" ExeCommand="/S" Return="check" />
. Replace UNQIUENAME with a unique name, NAMEOFBIN with the name you specified above in the binary tag. Then all you need to do is put the command line arguments in ExeCommand.
Finally you need to add it to the InstallExecuteSequence. To do this add an entry inside <Custom After="InstallInitialize" Action="InstallEXE">NOT REMOVE</Custom>
. This one is a little more complicated. What you need to do is replace InstallEXE with the action you want to schedule. The After attribute then specifies where in the InstallExecuteSequence the command should be executed. If you just want it to happen during install set it to InstallInitialize otherwise specify which custom action you want it to happen after.
The last thing you need to set is the text between the Custom tag to either NOT REMOVE
or REMOVE="All"
. NOT REMOVE will happen during install and REMOVE=“ALL” will happen during uninstall. There are a lot of other properties you can use for conditions but they are out of the scope of this guide.
Running EXE which already exists on the system
Another scenario you might want to do is execute an executable which already exists on the system. You might want to do this if you need to run an uninstall.exe file the application uses for uninstall.
To do that you can create a custom action (and replace the uninstallexe one if you want) with the following:
<CustomAction Id="MyCustomAction" Directory="MainAppDir" ExeCommand ='"[MainAppDir]Uninstall.exe" /S' Execute="deferred" Impersonate="no" Return="check" />
Now replace the following parts of the command above:
-
MyCustomAction - Replace this with a meaningful name that is unique. It is the name of the custom action you will need to add to InstallExecuteSequence like above.
-
Next you need to replace the Directory with the directory the command is located in. You can leave it as MainAppDir and I will show you how to specify the folder structure below.
-
EXECommand - This is the command you want to run. Key thing to see here is the following. notice the
"[MainAppDir]Unistall"
. This is specifying the full path to the executable you need to run, if you don’t do this it will fail. The [MainAppDir] part will resolve to whatever MainAppDir points to, this is better than hard coding the value because the directory may change on different systems (i.e. Program Files (x86)\Myapp on 64bit systems and just Program Files\My app on 32bit systems). You can also follow this up with command line arguments to make a silent uninstall. In this example /S.
Now you need to create a folder structure that the application will be located in. This needs to be put inside the existing Directory tags you already have in the project (starts with
<Directory Id="ProgramFilesFolder">
<Directory Id="MainAppDir" Name="MyApp">
</Directory>
</Directory>
In the above you just need to set Name to whatever you want the directory to be called on the file system. The Id is how you reference it in other parts of wix (like the CustomAction).
Finally the last step is to add the custom action to InstallExecuteSequence. See Extra actions section above for details on how to do this.
Running a PowerShell script as part of the install or uninstall.
The last extra scenario I am going to add here is how to call a custom script during the install/uninstall. This method is actually similar to the Running EXE which already exists on the system section above however it also has a file drop part to it.
So do this you need to create a directory structure with where you want to store the file (similar to how it was done above).
For example:
<Directory Id="ProgramFilesFolder">
<Directory Id="MainAppDir" Name="MyApp">
</Directory>
</Directory>
Once you have the directory structure created you can add a component into it with the file tag.
<Directory Id="ProgramFilesFolder">
<Directory Id="MainAppDir" Name="MyApp">
<Component Id="MyScript">
<File Id="myscript.ps1" Source="myscript.ps1" KeyPath="yes" Checksum="yes"/>
</Component>
</Directory>
</Directory>
Now to explain what each of the components are:
-
Component Id - This must be set to something unique. Just any descriptive name that describes the file.
-
FileId - This also needs to be unique. Just set it to the filename.
-
Source - This is where the file is currently located on your development/workstation machine. You should use a relative path. In this example the script is in the same directory as the wxs file.
Some notes on multiple files. You can add multiple files to the one component however only one of them can have keyPath set to yes.
Next up we need to add a ComponentRef to our MainFeature. To do this simply add <ComponentRef Id="MyScript" />
inside the Feature element.
Now we have a script file that will be dropped on the system during install and removed during uninstall. The final thing that needs to be done is to run the script. This can be done by following the steps above in Running EXE which already exists on the system.
Example:
<CustomAction Id="MyCustomAction" Directory="MainAppDir" ExeCommand ='"C:\windows\system32\WindowsPowerShell\v1.0\powershell.exe" -ExecutionPolicy Bypass -NonInteractive -NoProfile -File MyScript.ps1' Execute="deferred" Impersonate="no" Return="check" />
However there is one more thing you need to take into account when calling a script in this way. If your script is running as part of the Install it needs to happen after InstallFiles action.
<Custom After="InstallFiles" Action="MyCustomAction">NOT REMOVE</Custom>
If the script needs to be run during the uninstall then it needs to happen before RemoveFiles.
<Custom Before="RemoveFiles" Action="MyCustomAction">NOT REMOVE</Custom>
The reason why this sequencing is so important should be pretty obvious. You can’t run a script when its not there. InstallFiles puts the script on the system and RemoveFiles takes it away.