Introduction
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 an 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.
Technology Stack
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.
Solution Overview
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 build.bat file.
- 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
Using HEAT
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 WIXUI_INSTALLDIR
.
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.
Provision NGen
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 NativeImage
nodes?
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 DLL
or EXE
.
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
WixUI Library
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" />
When using 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>
Using CANDLE
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=1.0.0.0
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.
Using LIGHT
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
Summary
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.
History
- 22nd August, 2010: Initial post