It's been a while still my last article so I thought I'd write about my recent experiences with Wix for creating Windows Installer packages. I last used Wix 1.0 about 4 years ago with great results and had forgotten how satisfying it is to use. Unfortunately at the time of writing Wix 3.5 isn't out, but it sounds like there are some great additions in the pipeline including the much anticipated Burn utility (a bootstrap compiler), as well as Votive support for Visual Studio 2010.
In this series of articles I want to provide a complete end-to-end solution for deploying a .NET application based on my recent experiences. The solution provided is based around the following real-world requirements:
- Provide a MSI installer which deploys a .NET application
- Use NGen to pre-cache .NET assemblies for faster start-up times
- Only allow the installer to run on a specific OS
- Only allow the installer to run based on the results of an WMI lookup
- Only allow the installer to run if prerequisites are installed (.NET 4.0)
- Inject version info and other specifics into the installer from a build script
- Provide localized versions within a single MSI installer and auto detect the user language
- Create a localized bootstrapper as a single file that installs the necessary prerequisites (.NET 4.0)
Note because in my example I'm targeting a Visual Studio 2010 project which isn't supported by the Wix 3.0's Votive project, I'm instead using a NAnt script to automate the build process.
Note about GUIDs
It's important to generate your own GUIDs when working with Wix to avoid conflicts with other products. As it's common to copy & paste examples, I've deliberately masked my GUIDs to prevent any duplicates ending up in production code. To work with any of the examples in the solution, be sure to replace all ids of the format
NEWGUID-XXXX-XXXX-XXXX-XXXXXXXXXXXX with your own unique GUIDS. You'll find these have been factored into
Config.Wsi for convenience.
The MSI installer and EXE bootstrapper are produced from a single NAnt script. The script performs the following steps which are described in more detail throughout this series of articles. To build the project execute the included
- Use HEAT to produce a Wix component fragment
- Transform HEAT output to inject NGen elements
- Use CANDLE to compile the Wix project, injecting build variables
- Use LIGHT linker to generate an English version of the MSI
- Use LIGHT linker to generate a language variation of the MSI
- Use TORCH to create a transform (delta) file by comparing the output of steps 4 & 5
- Use WISUBSTG.VBS to embed the MST into the original MSI file
- Repeat steps 5-7 for each language supported
- Pre-process the dotNetInstaller config file to inject build parameters
- Compile the bootstrapper
The HEAT utility is used to generate Wix code fragments. The solution uses HEAT to create a Wix component fragment that describes the target files that will be deployed by the installer.
As we want the installer to deploy these files to the user's chosen installation directory, we need to nest the generated component element within a specific directory node that relates to this location. Luckily HEAT provides an argument for this, and we just need to pass the correct directory id into the tool using the
-dr parameter. As we will be using the WixUI library the installation directory id must be set to
heat dir MyProject\bin\Release -o Component.wsx -dr WIXUI_INSTALLDIR -cg AppFiles -var var.InputSourceFolder -g1 -gg -srd -sfrag
We also pass the
-cg argument so that HEAT writes out a generated
ComponentGroup with a specific id. This allows us to reference it from the main Wix project and include it in a Wix feature. The
var argument tells HEAT to swap the absolute paths to the target files with a custom variable
var.InputSourceFolder. This allows us to define the location of the target files at compile time.
The Native Image Generator (Ngen.exe) is a .NET tool that improves the performance of managed applications start-up time by pre-caching a compiled image. Normally with .NET applications the JIT compiler compiles the image on first run and therefore causes an initial delay. By running NGen against the application during installation the compiled .NET application image is pre-cached and will therefore execute faster on first launch.
Wix comes with the
WixNetFxExtension.dll extension that can be used to provision the NGen tool. To use the extension we just add the
netfx namespace to the
Main.wxs project file, and add the
NativeImage element to any file nodes that we want to pre-cache. Lastly we must ensure that we pass the extension to CANDLE during compilation.
The following shows how NGen is provisioned as a child of a parent file node within a component definition.
<Component Id="..." Guid="...."> <File Id="..." KeyPath="yes" Source="$(var.InputSourceFolder)\MyAppLibrary.dll"> <netfx:NativeImage Priority="1" Id="MyAppLibrary.dll" AppBaseDirectory="WIXUI_INSTALLDIR" /> </File> </Component>
However there's a problem, as HEAT is used to generate the component fragment dynamically, how can we then inject our
Well luckily Wix comes to the rescue in the form of an additional parameter that can be passed to the HEAT tool. This parameter describes an optional XSLT file that is applied to the fragment during output. This is used in the solution to inject the
NativeImage nodes into the component fragment for all files with an extension of
Our HEAT command now looks something like this:
heat.exe dir MyProject\bin\Release -o Component.wsx -dr WIXUI_INSTALLDIR -cg AppFiles -g1 -gg -srd -sfrag -t:Components.xslt
In the example solution provided I'm using the stock
WixUI_MONDO interface provided by the WixUI library. This is simply included in
Main.wsx with the following reference. In doing this we must include the
WixUIExtension.dll extension when executing LIGHT.
<UI> <UIRef Id="WixUI_Mondo" /> <UIRef Id="WixUI_ErrorProgressText" /> </UI>
In our solution we also swap out the stock images & agreement files used by the library dialogs by overriding some WixUI variables.
<WixVariable Id="WixUIBannerBmp" Value="binary\banner.bmp" /> <WixVariable Id="WixUIDialogBmp" Value="binary\dialog.bmp" /> <WixVariable Id="WixUILicenseRtf" Value="binary\EULA_en-gb.rtf" />
WixUI_Mondo the target installation directory defined in the source code must use an id of
WIXUI_INSTALLDIR. This relates back to the reference in the auto-generated
Component.wsx file, and allows a user to override the location via the WixUI interface. If the user does not customize the installation location then files are deployed to the folder within
Program Files as defined by the fragment below. This is using custom variables injected at compile time to determine the default folder names.
<Directory Id='TARGETDIR' Name='SourceDir'> <Directory Id='ProgramFilesFolder' Name='PFiles'> <Directory Id='ManufacturerDir' Name='$(var.CompanyName)'> <Directory Id='WIXUI_INSTALLDIR' Name='$(var.FriendlyName) v$(var.ProductVersion)' /> </Directory> </Directory> ... </Directory>
The Wix CANDLE utility compiles the Wix source files into a intermediate file format with a
WixOBJ extension. Build parameters can also be injected at this point as Wix custom define variables. We can use these to pass in the product name and version number, as well as the location of the target files to be included by the installer.
The CANDLE command looks something like this:
candle.exe -o bin\Release\ -ext WixNetFxExtension.dll -dInputSourceFolder=MyProject\bin\Release\ -dFriendlyName="My App" -dVersion=22.214.171.124 Main.wxs Component.wxs
In the solution provided you'll note I'm using a NAnt task provided by Wix for the CANDLE command. The build script is able to pass in the custom variables from build properties which could originate from a build server such as CruiseControl.
LIGHT is the linker used to generate the MSI file from the intermediate
WixObj libraries to produce a MSI file. The command line to do this in our solution would be as follows, but again in the solution we are actually using Wix NAnt task instead.
light.exe -o bin\Release\Setup.msi -ext WixNetFxExtension.dll -ext WixUIExtension.dll Main.wixobj Component.wixobj
In this part we have created a fully functional Windows Installer file for deploying the .NET application. In Part 2 I'll discuss adding condition checks to enhance the installer further. Part 2.