What is WiX Toolset & how to use it

What is WiX Toolset?

Windows Installer XML (WiX) Toolset is a Microsoft open source project used to create the Office 2007 installer. WiX includes some advanced capabilities, but it has a steep learning curve even though the scripting language is using XML format. It took me some time to learn both technologies (WiX and Windows Installer itself), but it’s worth spending the time.

I have been working with Windows-based installation development for some time and for some software projects that required a simple deployment, I preferred to create a basic MSI installer package whenever it’s possible. So, WiX Toolset will be the first choice.

Example screenshot

Installer UI - welcome dialog
Installer UI - welcome dialog

Advantages of using WiX Toolset

There are some advantages that I can think of when working with WiX Toolset for creating Windows app installer.

  • It’s a script-based setup using XML format. Better clarity, easy to edit and debug.
  • You will get a fresh clean MSI installer package. No extra stuff embedded, smaller in file size.
  • Everything can be done using command-line. Easy integration into your CI build system.
  • Can easily be source-controlled and versioning.
  • Good community support and a lot of examples.

Example of my WiX project structure

<root>
├── app\
|   ├── config.ini
|   ├── Eula-en.rtf
|   ├── filters.txt
|   └── MiniAppKiller.exe
├── images\
|   ├── app.ico
|   ├── Dialog.bmp
|   └── TopBanner.bmp
├── MakeInstaller.bat
├── Product.Loc-en.wxl
├── Product.Var.wxi
└── Poduct.wxs

Folders:

  • app - Contains my application files to be installed
  • images - Contains graphic resource for customizing the installer UI.

WiX project files:

  • Product.wxs - Main file
  • Product.Var.wxi - Variables file. Included automatically into the main file during compile time.
  • Product.Loc-en.wxl - Localization file for custom strings used in the main file
  • MakeInstaller.bat - The batch script to compile WiX project files

Example of my WiX preprocessor variables file

Below is the definitions for some variables that I need for creating my MSI package.

File name: Product.Var.wxi

<?xml version="1.0" encoding="utf-8"?>
<Include>
    <!-- #### USER-DEFINED VARIABLES THAT DEFINED DURING BUILD #### -->
    <?define AppVersion="$(var.BUILD_VERSION)" ?>
    <!-- Upgrade code HAS to be the same for all updates. Once you've chosen it, don't change it. -->
    <?define UpgradeCode="$(var.BUILD_GUID)" ?>
    <!-- Project directory -->
    <?define ProjectDir="$(var.BUILD_PROJECTDIR)" ?>

    <!-- #### USER-DEFINED VARIABLES #### -->
    <!-- Set to 1 if you want to enable major upgrade -->
    <?define EnableMajorUpgrade="1" ?>
    <?define AppExeName="MiniAppKiller.exe" ?>
    <!-- App Info -->
    <!-- The URL for Add/Remove Programs -->
    <?define InfoURL="https://heiswayi.nrird.com/" ?>
    <!-- If your app supports both platform (x86 & x64), you have to create separate MSI. -->
    <?define Platform="x86" ?>
    <!-- App Icon -->
    <?define AppIconSource="images\app.ico" ?>
    <!-- Other resources -->
    <!-- TopBanner.bmp: 499x58 px -->
    <?define ImageTopBannerSource="images\TopBanner.bmp" ?>
    <!-- Dialog.bmp: 499x301 px; Graphical area: 165x301 px -->
    <?define ImageDialogSource="images\Dialog.bmp" ?>
    <!-- GUIDs -->
    <?define GuidInstallLocation="3F638FF4-C84D-480F-83B8-31CCE0C10617" ?>
    <?define GuidApplicationShortcut="433B5301-96B9-4FD4-BD86-2A54FB5B9570" ?>
    <?define GuidApplicationShortcutDesktop="00EA4184-52A8-478D-8A89-47892454B0DD" ?>
    <?define GuidProductId="9996A526-5535-4DEB-BB56-B4D84BA96E38" ?>

    <!-- #### DO NOT EDIT ANYTHING BELOW THIS LINE #### -->
    <?if $(var.EnableMajorUpgrade) = 1 ?>
        <?define ProductId = "*" ?>
    <?else ?>
        <?define ProductId = "$(var.GuidProductId)" ?>
    <?endif ?>
    <?if $(var.Platform) = x64 ?>
        <?define Win64 = "yes" ?>
        <?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
        <?define MainExeSourcePath = "$(var.ProjectDir)\$(var.AppExeName)" ?>
    <?else ?>
        <?define Win64 = "no" ?>
        <?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
        <?define MainExeSourcePath = "$(var.ProjectDir)\$(var.AppExeName)" ?>
    <?endif ?>
</Include>

Note: BUILD_VERSION, BUILD_GUID and BUILD_PROJECTDIR are preprocessor variables that later will be used in MakeInstaller.bat file to compile the WiX files.

Example of my primary WiX file

Below is the script for my primary WiX file to create the MSI installer package. In this file, I have combined the code for customzing the installer UI which includes the following few custom dialogs:

  • Custom license agreement dialog
  • Custom upgrade dialog
  • Custom install dir dialog (for Desktop shortcut option checkbox)
With Create Desktop shortcut option
With Create Desktop shortcut option

These are some extra features other than the defaults that have been implemented into the script:

  • Detect for minimum .NET Framework and OS version.
  • Detect if newer version is installed, and abort the installation.
  • Detect if older version is installed, show Upgrade Welcome Dialog, and skip License Agreement Dialog.
  • Prompt user to close if the application executable file is running during upgrade.
  • Create registry to memorize the installation directory path.
  • Create Start Menu and Desktop shortcut. Desktop shortcut is optional, user can choose to not create it.

The script below is well-documented. It’s a complete script for my project. You will understand and get the picture how it works once you go all the way through the source code, line by line. (っ◕‿◕)っ

File name: Product.wxs

<?xml version="1.0" encoding="UTF-8"?>
<!-- Add xmlns:util namespace definition to be able to use stuff from WixUtilExtension.dll -->
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
    <!-- Include defined variables -->
    <?include Product.Var.wxi ?>
    <!-- The upgrade code must never change as long as the product lives! -->
    <!-- Product IDs must be autogenerated (*) or else major upgrades will not work -->
    <Product Id="$(var.ProductId)" Name="!(loc.ApplicationName)" Language="!(loc.Language)" Version="$(var.AppVersion)" Manufacturer="!(loc.ManufacturerName)" UpgradeCode="$(var.UpgradeCode)" >
        <!-- Package IDs are valid for a single package version only - they are autogenerated by WiX -->
        <!-- Let's require Windows Installer 4.0 (included in Vista) -->
        <!-- And ALWAYS install per machine!!! -->
        <Package Id="*" InstallerVersion="400" Compressed="yes" InstallPrivileges="elevated" InstallScope="perMachine"  Description="!(loc.ProductDescription)" Comments="!(loc.Comments) $(var.AppVersion)" />
        <!-- Define icons (ID should not be longer than 18 chars and must end with ".exe") -->
        <Icon Id="AppIcon.exe" SourceFile="$(var.AppIconSource)" />
        <!-- We do not have more than one medium (Floppy, CD, ...). Everything in one file. -->
        <Media Id="1" Cabinet="media1.cab" EmbedCab="yes" />
        <!-- Upgrade settings -->
        <Upgrade Id="$(var.UpgradeCode)">
            <UpgradeVersion OnlyDetect="yes" Minimum="$(var.AppVersion)" IncludeMinimum="no" Property="NEWER_VERSION_FOUND" />
            <UpgradeVersion Minimum="0.0.0.0" IncludeMinimum="yes" Maximum="$(var.AppVersion)" IncludeMaximum="no" Property="OLDER_VERSION_FOUND" />
        </Upgrade>

        <!-- License agreement text: dummy. Real text is set in WXS file -->
        <!--<WixVariable Id="WixUILicenseRtf" Value="dummy" />-->
        <!-- UI customization -->
        <WixVariable Id="WixUIBannerBmp" Value="$(var.ImageTopBannerSource)" />
        <WixVariable Id="WixUIDialogBmp" Value="$(var.ImageDialogSource)" />
        <Binary Id="imageDialog" SourceFile="$(var.ImageDialogSource)" />
        <!-- Set properties for Add/Remove Programs -->
        <Property Id="ARPPRODUCTICON" Value="AppIcon.exe" />
        <Property Id="ARPHELPLINK" Value="$(var.InfoURL)" />
        <!-- Remove repair -->
        <!--<Property Id="ARPNOREPAIR" Value="yes" Secure="yes" />-->
        <!-- Remove modify -->
        <!--<Property Id="ARPNOMODIFY" Value="yes" Secure="yes" />-->
        <Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR" />
        <Property Id="DialogBitmap">imageDialog</Property>
        <!-- Determine the directory of a previous installation (if one exists). If not INSTALLDIR stays empty -->
        <Property Id="INSTALLDIR">
            <RegistrySearch Id="DetermineInstallLocation" Type="raw" Root="HKLM" Key="Software\!(loc.ManufacturerName)\InstalledProducts\!(loc.ApplicationName)" Name="InstallLocation" />
        </Property>
        <Property Id="UpgradeDlgHasShown" Value="0" />
        <Property Id="ADDDESKTOPSHORTCUT" Value="1"/> 

        <!-- Reference the global NETFRAMEWORK35 property to check if it exists -->
        <PropertyRef Id="NETFRAMEWORK35"/>    
        <!-- 
        Startup conditions that checks if .Net Framework 3.5 is installed or if 
        we're running the OS higher than Windows XP SP2.
        If not the installation is aborted.
        By doing the (Installed OR ...) property means that this condition will only 
        be evaluated if the app is being installed and not on uninstall or changing
        -->
        <Condition Message="!(loc.DotNetFrameworkNeeded)">
            <![CDATA[Installed OR NETFRAMEWORK35]]>
        </Condition>
        <Condition Message="!(loc.AppNotSupported)">
            <![CDATA[Installed OR ((VersionNT >= 501 AND ServicePackLevel >= 2) OR (VersionNT >= 502))]]>
        </Condition>

        <!-- Set up ARPINSTALLLOCATION property (http://blogs.technet.com/b/alexshev/archive/2008/02/09/from-msi-to-wix-part-2.aspx) -->
        <CustomAction Id="SetARPINSTALLLOCATION" Property="ARPINSTALLLOCATION" Value="[INSTALLDIR]" />
        <!-- Save the command line value INSTALLDIR and restore it later in the sequence or it will be overwritten by the value saved to the registry during an upgrade -->
        <!-- http://robmensching.com/blog/posts/2010/5/2/the-wix-toolsets-remember-property-pattern/ -->
        <CustomAction Id='SaveCmdLineValueINSTALLDIR' Property='CMDLINE_INSTALLDIR' Value='[INSTALLDIR]' Execute='firstSequence' />
        <CustomAction Id='SetFromCmdLineValueINSTALLDIR' Property='INSTALLDIR' Value='[CMDLINE_INSTALLDIR]' Execute='firstSequence' />
        <CustomAction Id="NewerVersionFound" Error="!(loc.NewerVersionInstalled)" />

        <!-- UI Sequence -->
        <InstallUISequence>
            <!-- Check for newer versions with FindRelatedProducts and execute the custom action after it -->
            <Custom Action="NewerVersionFound" After="FindRelatedProducts">
                <![CDATA[NEWER_VERSION_FOUND]]>
            </Custom>
            <Custom Action='SaveCmdLineValueINSTALLDIR' Before='AppSearch' />
            <Custom Action='SetFromCmdLineValueINSTALLDIR' After='AppSearch'>CMDLINE_INSTALLDIR</Custom>
        </InstallUISequence>
        
        <!-- Exec Sequence -->
        <!-- This is the main installer sequence run when the product is actually installed -->
        <InstallExecuteSequence>
            <!-- Check for newer versions with FindRelatedProducts and execute the custom action after it -->
            <Custom Action="NewerVersionFound" After="FindRelatedProducts">
                <![CDATA[NEWER_VERSION_FOUND]]>
            </Custom>
            <!-- Remove the previous versions of the product -->
            <RemoveExistingProducts After="InstallInitialize"/>
            <!-- WixCloseApplications is a built in custom action that uses util:CloseApplication below -->
            <Custom Action="WixCloseApplications" Before="InstallInitialize" />
            <!-- Determine the install location after the install path has been validated by the installer -->
            <Custom Action="SetARPINSTALLLOCATION" After="InstallValidate"></Custom>
            <Custom Action='SaveCmdLineValueINSTALLDIR' Before='AppSearch' />
            <Custom Action='SetFromCmdLineValueINSTALLDIR' After='AppSearch'>CMDLINE_INSTALLDIR</Custom>
        </InstallExecuteSequence>

        <!-- This will ask the user to close the app if it's running while upgrading -->
        <util:CloseApplication Id="CloseApp" CloseMessage="no" Description="!(loc.CloseRunningApp)" ElevatedCloseMessage="no" RebootPrompt="no" Target="$(var.AppExeName)" />
        
        <!-- Outermost folder (kind of virtual). Fixed entry. -->
        <Directory Id="TARGETDIR" Name="SourceDir">
            <!-- We start building our directory structure here -->
            <!-- "ProgramFilesFolder" is a variable containing the absolute path. -->
            <!-- For a list of folder variables, see: http://msdn.microsoft.com/en-us/library/aa372057%28VS.85%29.aspx -->
            <Directory Id="$(var.PlatformProgramFilesFolder)">
                <!-- All folders from here on are relative to their parent. -->
                <Directory Id="MyProgramFiles" Name="!(loc.ManufacturerName)">
                    <!-- INSTALLDIR is a property name. We need it later for the UI (to be able to change the install dir. -->
                    <Directory Id="INSTALLDIR" Name="!(loc.ApplicationName)"></Directory>
                </Directory>
            </Directory>
            <!-- Registry entries -->
            <Component Id="RegValInstallLocation_comp" Guid="$(var.GuidInstallLocation)">
                <!-- Do NOT use the application's default registry key here, because THIS key will be removed on uninstall
                 (important when installing a newer version, because that is uninstall followed by install) -->
                <RegistryKey Root="HKLM" Key="Software\!(loc.ManufacturerName)\InstalledProducts\!(loc.ApplicationName)">
                    <RegistryValue Name="InstallLocation" Value="[INSTALLDIR]" Type="string" KeyPath="yes" />
                </RegistryKey>
            </Component>
            <!-- Shortcut folders -->
            <Directory Id="ProgramMenuFolder">
                <Directory Id="ApplicationProgramsFolder" Name="!(loc.ApplicationName)" /></Directory>
            <Directory Id="DesktopFolder" Name="Desktop"></Directory>
        </Directory>
        <!-- Shortcut components -->
        <DirectoryRef Id="ApplicationProgramsFolder">
            <Component Id="ApplicationShortcut" Guid="$(var.GuidApplicationShortcut)">
                <Shortcut Id="ApplicationStartMenuShortcut" Name="!(loc.ApplicationName)" Description="!(loc.ProductDescription)" Target="[INSTALLDIR]$(var.AppExeName)" WorkingDirectory="INSTALLFOLDER" />
                <RemoveFolder Id="RemoveApplicationProgramsFolder" Directory="ApplicationProgramsFolder" On="uninstall" />
                <RegistryValue Root="HKCU" Key="Software\!(loc.ManufacturerName)\InstalledProducts\!(loc.ApplicationName)" Name="installed" Type="integer" Value="1" KeyPath="yes" />
            </Component>
        </DirectoryRef>
        <DirectoryRef Id="DesktopFolder">
            <Component Id="ApplicationShortcutDesktop" Guid="$(var.GuidApplicationShortcutDesktop)">
                <Shortcut Id="ApplicationDesktopShortcut" Name="!(loc.ApplicationName)" Description="!(loc.ProductDescription)" Target="[INSTALLDIR]$(var.AppExeName)" WorkingDirectory="INSTALLFOLDER" />
                <RemoveFolder Id="RemoveDesktopFolder" Directory="DesktopFolder" On="uninstall" />
                <RegistryValue Root="HKCU" Key="Software\!(loc.ManufacturerName)\InstalledProducts\!(loc.ApplicationName)" Name="installed" Type="integer" Value="1" KeyPath="yes" />
                <Condition>ADDDESKTOPSHORTCUT</Condition>
            </Component>
        </DirectoryRef>
        <!-- Features define which parts of the application can be installed in a custom installation -->
        <Feature Id="Complete" Title="!(loc.ApplicationName)" Description="!(loc.FeatureCompleteDescription)" Display="expand" Level="1" ConfigurableDirectory="INSTALLDIR">
            <!-- A feature block for the main (GUI) program and all its dependencies -->
            <Feature Id="MainProgram" Title="!(loc.FeatureMainProgramTitle)" Description="!(loc.FeatureMainProgramDescription)" Level="1">
                <!-- Installation folder: Generated automatically by heat.exe -->
                <ComponentGroupRef Id="INSTALLDIR_comp" />
                <!-- Registry entries -->
                <ComponentRef Id="RegValInstallLocation_comp" />
                <!-- Shortcuts -->
                <ComponentRef Id="ApplicationShortcut" />
                <ComponentRef Id="ApplicationShortcutDesktop" />
            </Feature>
        </Feature>
        <UI Id="MyWixUI">
            <TextStyle Id="WixUI_Font_Normal" FaceName="Tahoma" Size="8" />
            <TextStyle Id="WixUI_Font_Bigger" FaceName="Tahoma" Size="12" />
            <TextStyle Id="WixUI_Font_Title" FaceName="Tahoma" Size="9" Bold="yes" />
            <Property Id="DefaultUIFont" Value="WixUI_Font_Normal" />
            <Property Id="WixUI_Mode" Value="InstallDir" />
            <DialogRef Id="BrowseDlg" />
            <DialogRef Id="DiskCostDlg" />
            <DialogRef Id="ErrorDlg" />
            <DialogRef Id="FatalError" />
            <DialogRef Id="FilesInUse" />
            <DialogRef Id="MsiRMFilesInUse" />
            <DialogRef Id="PrepareDlg" />
            <DialogRef Id="ProgressDlg" />
            <DialogRef Id="ResumeDlg" />
            <DialogRef Id="UserExit" />
            <Dialog Id="CustomLicenseAgreementDlg" Width="370" Height="270" Title="!(loc.LicenseAgreementDlg_Title)">
                <Control Id="LicenseAcceptedCheckBox" Type="CheckBox" X="20" Y="207" Width="330" Height="18" CheckBoxValue="1" Property="LicenseAccepted" Text="!(loc.LicenseAgreementDlgLicenseAcceptedCheckBox)" />
                <Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="!(loc.WixUIBack)" />
                <Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="!(loc.WixUINext)">
                    <Publish Event="SpawnWaitDialog" Value="WaitForCostingDlg">CostingComplete = 1</Publish>
                    <Condition Action="disable">
                        <![CDATA[LicenseAccepted <> "1"]]>
                    </Condition>
                    <Condition Action="enable">LicenseAccepted = "1"</Condition>
                </Control>
                <Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="!(loc.WixUICancel)">
                    <Publish Event="SpawnDialog" Value="CancelDlg">1</Publish>
                </Control>
                <Control Id="BannerBitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="44" TabSkip="no" Text="!(loc.LicenseAgreementDlgBannerBitmap)" />
                <Control Id="LicenseText" Type="ScrollableText" X="20" Y="60" Width="330" Height="140" Sunken="yes" TabSkip="no">
                    <!-- This is the original line -->
                    <!--<Text SourceFile="!(wix.WixUILicenseRtf=$(var.LicenseRtf))" />-->
                    <!-- To enable EULA localization we change it to this: -->
                    <Text SourceFile="$(var.ProjectDir)\!(loc.LicenseRtf)" />
                    <!-- In each of the localization files (wxl) put a line like this:
            <String Id="LicenseRtf" Overridable="yes">EULA_en-us.rtf</String>-->
                </Control>
                <Control Id="Print" Type="PushButton" X="112" Y="243" Width="56" Height="17" Text="!(loc.WixUIPrint)">
                    <Publish Event="DoAction" Value="WixUIPrintEula">1</Publish>
                </Control>
                <Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" />
                <Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
                <Control Id="Description" Type="Text" X="25" Y="23" Width="340" Height="15" Transparent="yes" NoPrefix="yes" Text="!(loc.LicenseAgreementDlgDescription)" />
                <Control Id="Title" Type="Text" X="15" Y="6" Width="200" Height="15" Transparent="yes" NoPrefix="yes" Text="!(loc.LicenseAgreementDlgTitle)" />
            </Dialog>

            <Dialog Id="CustomUpgradeDlg" Width="370" Height="270" Title="!(loc.UpgradeWelcomeDlgTitle)" NoMinimize="yes">
                <Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Disabled="yes" Text="!(loc.WixUIBack)">
                    <Publish Event="NewDialog" Value="WelcomeDlg">1</Publish>
                </Control>
                <Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="!(loc.WixUINext)">
                    <Publish Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
                    <Publish Property="UpgradeDlgHasShown" Value="1">1</Publish>
                </Control>
                <Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="!(loc.WixUICancel)">
                    <Publish Event="SpawnDialog" Value="CancelDlg">1</Publish>
                </Control>
                <Control Id="Bitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="234" TabSkip="no" Text="[DialogBitmap]" />
                <Control Id="BottomLine" Type="Line" X="0" Y="234" Width="374" Height="0" />
                <Control Id="Description" Type="Text" X="135" Y="70" Width="220" Height="30" Transparent="yes" NoPrefix="yes">
                    <Text>!(loc.UpgradeWelcomeDlgMessage)</Text>
                </Control>
                <Control Id="Title" Type="Text" X="135" Y="20" Width="220" Height="60" Transparent="yes" NoPrefix="yes">
                    <Text>{\WixUI_Font_Bigger}!(loc.UpgradeWelcomeDlgHeaderTitle)</Text>
                </Control>
            </Dialog>

            <Dialog Id="CustomInstallDirDlg" Width="370" Height="270" Title="!(loc.InstallDirDlg_Title)">
                <Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="!(loc.WixUINext)" />
                <Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="!(loc.WixUIBack)" />
                <Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="!(loc.WixUICancel)">
                    <Publish Event="SpawnDialog" Value="CancelDlg">1</Publish>
                </Control>

                <Control Id="Description" Type="Text" X="25" Y="23" Width="280" Height="15" Transparent="yes" NoPrefix="yes" Text="!(loc.InstallDirDlgDescription)" />
                <Control Id="Title" Type="Text" X="15" Y="6" Width="200" Height="15" Transparent="yes" NoPrefix="yes" Text="!(loc.InstallDirDlgTitle)" />
                <Control Id="BannerBitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="44" TabSkip="no" Text="!(loc.InstallDirDlgBannerBitmap)" />
                <Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" />
                <Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />

                <Control Id="FolderLabel" Type="Text" X="20" Y="60" Width="290" Height="30" NoPrefix="yes" Text="!(loc.InstallDirDlgFolderLabel)" />
                <Control Id="Folder" Type="PathEdit" X="20" Y="100" Width="320" Height="18" Property="WIXUI_INSTALLDIR" Indirect="yes" />
                <Control Id="ChangeFolder" Type="PushButton" X="20" Y="120" Width="56" Height="17" Text="!(loc.InstallDirDlgChange)" />
                <Control Id="DesktopShortcutCheckBox" Type="CheckBox" X="20" Y="160" Width="290" Height="17" Property="ADDDESKTOPSHORTCUT" CheckBoxValue="1" Text="!(loc.CreateDesktopShortcutCheckboxText)"/> 
            </Dialog>

            <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="CustomLicenseAgreementDlg">NOT Installed</Publish>

            <!--<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg">(Installed AND PATCH) OR WIX_UPGRADE_DETECTED</Publish>-->
            <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="CustomUpgradeDlg">(Installed AND PATCH) OR OLDER_VERSION_FOUND</Publish>

            <Publish Dialog="CustomLicenseAgreementDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg">1</Publish>
            <Publish Dialog="CustomLicenseAgreementDlg" Control="Next" Event="NewDialog" Value="CustomInstallDirDlg">LicenseAccepted = "1"</Publish>
            <Publish Dialog="CustomInstallDirDlg" Control="Back" Event="NewDialog" Value="CustomLicenseAgreementDlg">1</Publish>
            <Publish Dialog="CustomInstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
            <Publish Dialog="CustomInstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath" Order="2">NOT WIXUI_DONTVALIDATEPATH</Publish>
            <Publish Dialog="CustomInstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3">
                <![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"]]>
            </Publish>
            <Publish Dialog="CustomInstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="4">WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1"</Publish>
            <Publish Dialog="CustomInstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
            <Publish Dialog="CustomInstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2">1</Publish>
            <Publish Dialog="CustomInstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
            <Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath" Order="3">1</Publish>
            <Publish Dialog="BrowseDlg" Control="OK" Event="SpawnDialog" Value="InvalidDirDlg" Order="4">
                <![CDATA[WIXUI_INSTALLDIR_VALID<>"1"]]>
            </Publish>
            <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="CustomInstallDirDlg" Order="1">NOT Installed</Publish>
            <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="2">Installed</Publish>
            
            <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="CustomUpgradeDlg" Order="2">UpgradeDlgHasShown = "1"</Publish>

            <Publish Dialog="MaintenanceWelcomeDlg" Control="Next" Event="NewDialog" Value="MaintenanceTypeDlg">1</Publish>
            <Publish Dialog="MaintenanceTypeDlg" Control="RepairButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
            <Publish Dialog="MaintenanceTypeDlg" Control="RemoveButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
            <Publish Dialog="MaintenanceTypeDlg" Control="Back" Event="NewDialog" Value="MaintenanceWelcomeDlg">1</Publish>
            <Publish Dialog="ExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999">1</Publish>
        </UI>
        <UIRef Id="WixUI_Common" />
    </Product>
</Wix>

More example screenshots

License agreement dialog
License agreement dialog
When existing version already installed - upgrade dialog
When existing version already installed - upgrade dialog

Example of the localization file used for the WiX project

File name: Product.Loc-en.wxl

<?xml version="1.0" encoding="utf-8"?>
<WixLocalization Culture="en-us" Codepage="1252" xmlns="http://schemas.microsoft.com/wix/2006/localization">
    <String Id="Language">1033</String>
    <!-- Supported language and codepage codes can be found here: http://www.tramontana.co.hu/wix/lesson2.php#2.4 -->
    <String Id="ApplicationName">MiniAppKiller</String>
    <String Id="ManufacturerName">Heiswayi Nrird</String>
    <String Id="ProductDescription">Process killer</String>
    <String Id="Comments">Installs MiniAppKiller</String>

    <String Id="LicenseRtf" Overridable="yes">Eula-en.rtf</String>

    <String Id="AppNotSupported">This application is is not supported on your current OS. Minimal OS supported is Windows XP SP2</String>
    <String Id="DotNetFrameworkNeeded">.NET Framework 3.5 is required. Please install the .NET Framework then run this installer again.</String>
    <String Id="NewerVersionInstalled">A newer version of !(loc.ApplicationName) is already installed.</String>
    <String Id="CloseRunningApp">!(loc.ApplicationName) is running. You need to close it first!</String>

    <String Id="FeatureCompleteDescription">The complete package.</String>
    <String Id="FeatureMainProgramDescription">The main version including all dependencies.</String>
    <String Id="FeatureMainProgramTitle">Main program</String>

    <!--<String Id="LicenseAgreementDlgLicenseAcceptedCheckBox">Agree</String>-->

    <String Id="UpgradeWelcomeDlgTitle">!(loc.ApplicationName) Setup</String>
    <String Id="UpgradeWelcomeDlgHeaderTitle">A previous version of !(loc.ApplicationName) is detected</String>
    <String Id="UpgradeWelcomeDlgMessage">The setup will perform an upgrade of !(loc.ApplicationName). Click Next to proceed with the upgrade or Cancel to exit the setup.</String>

    <String Id="CreateDesktopShortcutCheckboxText">Create a shortcut on Desktop</String>
</WixLocalization>

How to create a batch script to compile the WiX project

These are the WiX tools that I used in the batch script below:

  • heat.exe - To automatically harvest my application files and generate a collection of <Component> elements and <ComponentGroup> that will be used in Product.wxs file under <Feature> element. Output from this tool is Product.Files.wxs file.
  • candle.exe - To generate *.wixobj file with some preprocessor variables.
  • light.exe - To compile *.wixobj and localization file using WiX extensions and generate *.msi file.

File name: MakeInstaller.bat

This batch script will generate/compile three different versions of MSI installer package for my testing purpose.

@echo off

set projectDir=.

rem Automatically generated fragment file for application files
"%WIX%bin\heat.exe" dir "app" -cg INSTALLDIR_comp -gg -scom -sreg -sfrag -srd -dr INSTALLDIR -var var.ProjectDir -out "Product.Files.wxs"
"%WIX%bin\candle.exe" "Product.Files.wxs" -out "_Product.Files.wixobj" -dProjectDir="%projectDir%\app"

rem Create setup-1.0.msi
"%WIX%bin\candle.exe" "Product.wxs" -out "_Product.wixobj" -dBUILD_GUID="2BEA883D-BB4C-4A70-B668-88AA54025F5A" -dBUILD_VERSION="1.0.0.0" -dBUILD_PROJECTDIR="%projectDir%\app" -ext WixUtilExtension -nologo
"%WIX%bin\light.exe" "_Product.Files.wixobj" "_Product.wixobj" -loc "Product.Loc-en.wxl" -cultures:en-US -ext WixUtilExtension -ext WixUIExtension -ext WixNetFxExtension -out "setup-1.0.msi" -nologo

rem Create setup-1.1.msi
"%WIX%bin\candle.exe" "Product.wxs" -out "_Product.wixobj" -dBUILD_GUID="2BEA883D-BB4C-4A70-B668-88AA54025F5A" -dBUILD_VERSION="1.1.0.0" -dBUILD_PROJECTDIR="%projectDir%\app" -ext WixUtilExtension -nologo
"%WIX%bin\light.exe" "_Product.Files.wixobj" "_Product.wixobj" -loc "Product.Loc-en.wxl" -cultures:en-US -ext WixUtilExtension -ext WixUIExtension -ext WixNetFxExtension -out "setup-1.1.msi" -nologo

rem Create setup-2.0.msi
"%WIX%bin\candle.exe" "Product.wxs" -out "_Product.wixobj" -dBUILD_GUID="2BEA883D-BB4C-4A70-B668-88AA54025F5A" -dBUILD_VERSION="2.0.0.0" -dBUILD_PROJECTDIR="%projectDir%\app" -ext WixUtilExtension -nologo
"%WIX%bin\light.exe" "_Product.Files.wixobj" "_Product.wixobj" -loc "Product.Loc-en.wxl" -cultures:en-US -ext WixUtilExtension -ext WixUIExtension -ext WixNetFxExtension -out "setup-2.0.msi" -nologo

Final project structure

Here’s the final project structure after I run MakeInstaller.bat script:

Project structure after compile

Here’s the example screenshot showing the installation progress dialog after I run the .msi file and proceed with the installation:

Installation progress dialog
Installation progress dialog

That’s how I created a WiX project for one of my Windows-based desktop apps. If you’re new to WiX Toolset, you can follow and use the given examples and snippets above and modify the code/script to suit your requirement.

Note: This project is based on WiX v3.11 (GitHub) at the time I’m writing this post. If you need to customize your installer UI dialogs or to support multiple localizations, you can refer to this repository on GitHub for more references on the source code.