Click here to Skip to main content
15,884,425 members
Articles / Windows Installer

Creating a Localized Windows Installer & Bootstrapper: Part 1

Rate me:
Please Sign up or sign in to vote.
4.75/5 (9 votes)
22 Aug 2010CPOL6 min read 45K   734   41   5
This series of articles is a complete end-to-end solution for building a localizable Windows Installer & Bootstrapper using some real-world requirements.

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.

  1. Use HEAT to produce a Wix component fragment
  2. Transform HEAT output to inject NGen elements
  3. Use CANDLE to compile the Wix project, injecting build variables
  4. Use LIGHT linker to generate an English version of the MSI
  5. Use LIGHT linker to generate a language variation of the MSI
  6. Use TORCH to create a transform (delta) file by comparing the output of steps 4 & 5
  7. Use WISUBSTG.VBS to embed the MST into the original MSI file
  8. Repeat steps 5-7 for each language supported
  9. Pre-process the dotNetInstaller config file to inject build parameters
  10. 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.

XML
<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.

HTML
<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.

XML
<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.

XML
<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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
United Kingdom United Kingdom
Mike Carlisle - Technical Architect with over 20 years experience in a wide range of technologies.

@TheCodeKing

Comments and Discussions

 
Questionbuild.bat failed Pin
briankp9-Apr-13 7:25
briankp9-Apr-13 7:25 
AnswerRe: build.bat failed Pin
TheCodeKing10-Apr-13 7:17
TheCodeKing10-Apr-13 7:17 
GeneralMy vote of 5 Pin
zbudi23-Nov-11 21:27
zbudi23-Nov-11 21:27 
Generallearned something Pin
dB.30-Aug-10 14:31
dB.30-Aug-10 14:31 
GeneralMy vote of 5 Pin
sam.hill22-Aug-10 16:06
sam.hill22-Aug-10 16:06 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.