WiX: Managed Custom Actions

I've added WiX to my build process to automate the production of windows installer packages for my product. I'm not very proficient with Windows Installer technology, so I thought I might get by with what I've seen is already a "traditional" approach: dark an MSI created by a Visual Studio Setup Project. Well, I knew that this was not the right approach the moment I laid eyes on the script file I got from the decompiler.

However, it was sufficient to familiarize myself with the general structure of a WiX script. Soon after I decided to drop that script and start fresh. After a few dead-ends (that's when I used to refer to the Windows Installer SDK) I got my script working.

My thoughts on the subject are:

  1. Don't rely on the script you get from dark. It's confusing and things like setting up a virtual directory on a web server can be done with the WiX built-in custom actions in a very simple way;
  2. Debugging isn't easy and most of the time errors are unclear (However, from Rob's brownbag video, I suppose this is only temporary as there are plans to hook the WiX compiler to Visual Studio);
  3. The WiX documentation (for the schema) and the Windows Installer SDK will be your best friends while writing the script, so keep them handy.

 

Using Managed Custom Actions with WiX

Incorporating managed Custom Actions in WiX is a relatively simple task. However, you have to pay attention to a few details to avoid ending up with a headache just trying to figure out what's wrong with the script.

The first thing I did was making sure that the installer class was in the package, so I added a file reference for the assembly with the installer type.

    1: <File Id="InstallerFile" Name="SAMPLE~1.DLL" LongName="SampleInstaller.dll" src="$(var.BinFolder)\SampleInstaller.dll" Vital="yes" KeyPath="yes" DiskId="1" />

What's worth noting here is the specific Id for the file. I did this because it was necessary to reference that specific file later in the script, otherwise I just stuck with the Ids I got from tallow (for the rest of the files in the package).

To setup WiX to execute a managed installer class as a Custom Action, I used 'InstallUtil'. This tool can be run from the command-line ('installutil.exe'), but what I really wanted to use was 'InstallUtilLib.dll'. In order to do this I added the following reference to the script:

    1: <Binary Id="InstallUtil" src='$(var.BinFolder)\Installer Assemblies\InstallUtilLib.dll' />

If you've ever darked (decompiler in the WiX toolset) a VS Setup Project that uses managed custom actions, you're probably familiar with the following script entries:

    1: <Binary Id="VSDNETCFG" src="Binary\VSDNETCFG.ibd" />

This entry is used to produce a variable that is passed as part of the command-line arguments of installutillib. The variable is [VSDFxConfigFile] in the darked script.

Upon close inspection this embedded file (VSDNETCFG.ibd) is a simple config file that provides run-time binding information. So, I just added it to the installation package:

    1: <File Id="ConfigFile" Name="SAMPLE~2.XML" LongName="SampleInstaller.dll.config" src="$(var.BinFolder)\SampleInstaller.dll.config" Vital="yes" DiskId="1" />

Setting up the Custom Action is simple, albeit not trivial. The Custom Actions are calls to installutillib. My advice is to always check these using "InstallUtil.exe" via the command-line, to make sure they're working ok.

    1: <CustomAction Id='Uninstall' BinaryKey='InstallUtil' DllEntry='ManagedInstall' Execute='deferred' />
    2: <CustomAction Id='UninstallSetProp' Property='Uninstall' Value=' /installtype=notransaction /action=uninstall /LogFile= "[#InstallerFile]" "[#ConfigFile]"' />
    3:  
    4: <CustomAction Id='Install' BinaryKey='InstallUtil' DllEntry='ManagedInstall' Execute='deferred' />
    5: <CustomAction Id='InstallSetProp' Property='Install' Value=' /installtype=notransaction /action=install /LogFile= /BinDir="[TARGETDIR]\" /Package="[ProductName]" "[#InstallerFile]" "[#ConfigFile]"' />
    6:  
    7: <CustomAction Id='Rollback' BinaryKey='InstallUtil' DllEntry='ManagedInstall' Execute='rollback' />
    8: <CustomAction Id='RollbackSetProp' Property='Rollback' Value=' /installtype=notransaction /action=rollback /LogFile= "[#InstallerFile]" "[#ConfigFile]"' />
    9:  
   10: <CustomAction Id='Commit' BinaryKey='InstallUtil' DllEntry='ManagedInstall' Execute='commit' />
   11: <CustomAction Id='CommitSetProp' Property='Commit' Value=' /installtype=notransaction /action=commit /LogFile= "[#InstallerFile]" "[#ConfigFile]"' />

There are a few particulars to note in the script fragment above:

  1. The values of the properties are passed to the custom action due to the fact that the Id and Property attributes match (so, make sure they do).

 

  1. Make sure that the last parameter is the config file mentioned before. If this file cannot be found you'll probably get the following error:

 InstallUtilLib.dll: Unknown error in CorBindToRuntimeHost (0x80131700).
  1. If you wish to pass some data to your custom action, like the parameters /BinDir or /Package in the install Custom Action above, make sure that if you're passing some Windows Installer property that returns a directory you enclose it in quotes and include a trailing backslash (https://msdn.microsoft.com/library/default.asp?url=/library/en-us/vsintro7/html/vxgrfcustomactiondataproperty.asp). If you don't, you might find yourself struggling with the following error:

System.IO.FileNotFoundException: File or assembly name <assembly>, or one of its dependencies, was not found.

After defining the Custom Actions all that's left is to sequence them in InstallExecuteSequence:

    1: <Custom Action="InstallSetProp" After="StartServices">$SampleComponent&gt;2</Custom>
    2: <Custom Action="Install" After="InstallSetProp">$SampleComponent&gt;2</Custom>
    3:  
    4: <Custom Action="UninstallSetProp" After="MsiUnpublishAssemblies">$SampleComponent=2</Custom>
    5: <Custom Action="Uninstall" After="UninstallSetProp">$SampleComponent=2</Custom>
    6:  
    7: <Custom Action="CommitSetProp" After="Rollback">$SampleComponent&gt;2</Custom>
    8: <Custom Action="Commit" After="CommitSetProp">$SampleComponent&gt;2</Custom>
    9:  
   10: <Custom Action="RollbackSetProp" After="Install">$SampleComponent&gt;2</Custom>
   11: <Custom Action="Rollback" After="RollbackSetProp">$SampleComponent&gt;2</Custom>

In the end I was surprised at how simple, compact and clean I got my script, especially when compared to the one I got from dark. On another note I have to say that the WiX built-in Custom Actions were a huge help.