Creating a simpler MSI setup

In my previous blog post “Create a basic MSI installer using WiX Toolset”, I shared how I create a basic MSI installer for my app, however the WiX project that I shared, the example of the WiX code is a little bit of exaggerating. This is because I need to add some extra features and also some customizations to the setup dialogs. Well, if file copy is the only thing you need, perhaps this article can guide you through in creating a simpler MSI setup. This tutorial is based on WiX Toolset Compiler 3.11.1.2318.

Getting started

To get started, you will need a Product element and this is the main structure of your WiX code:

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">

    <!-- We'll include some preprocessor variables here -->

    <Product>
        <!-- Anything goes here will define the design of our setup -->
    </Product>

</Wix>

Let’s add some preprocessor variables and our <Product> attributes:

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">

    <?define MyProductName = "ExampleApp" ?>
    <?define MyProductVersion = "1.0.0" ?>
    <?define MyProductCode = "*" ?>
    <?define MyUpgradeCode = "DA3EEBC7-5812-4380-BDAE-1605417230CB" ?>
    <?define ManufacturerName = "Heiswayi Nrird" ?>
    <?define ProductDescription = "Simplest MSI setup" ?>
    <?define ProductCopyright = "(c) 2018 $(var.ManufacturerName)" ?>
    <?define MainProgramFileFullname = "ExampleApp.exe" ?>
    <?define GuidProgramShortcut = "9ACF08F5-CFF4-43C1-84F9-418ADAAD772D" ?>
    <?define GuidProgramShortcut64 = "CEA4CD03-0819-4A30-903A-22E09B254F04" ?>

    <Product Id="$(var.MyProductCode)" 
             Name="$(var.MyProductName)"
             Language="1033" 
             Version="$(var.MyProductVersion)" 
             Manufacturer="$(var.ManufacturerName)" 
             UpgradeCode="$(var.MyUpgradeCode)">
        <!-- Anything goes here will define the design of our setup -->
    </Product>

</Wix>

Now let’s define our MSI setup <Package> and <MediaTemplate> into our <Product>:

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">

    <?define MyProductName = "ExampleApp" ?>
    <?define MyProductVersion = "1.0.0" ?>
    <?define MyProductCode = "*" ?>
    <?define MyUpgradeCode = "DA3EEBC7-5812-4380-BDAE-1605417230CB" ?>
    <?define ManufacturerName = "Heiswayi Nrird" ?>
    <?define ProductDescription = "Simplest MSI setup" ?>
    <?define ProductCopyright = "(c) 2018 $(var.ManufacturerName)" ?>
    <?define MainProgramFileFullname = "ExampleApp.exe" ?>
    <?define GuidProgramShortcut = "9ACF08F5-CFF4-43C1-84F9-418ADAAD772D" ?>
    <?define GuidProgramShortcut64 = "CEA4CD03-0819-4A30-903A-22E09B254F04" ?>

    <Product Id="$(var.MyProductCode)" 
             Name="$(var.MyProductName)"
             Language="1033" 
             Version="$(var.MyProductVersion)" 
             Manufacturer="$(var.ManufacturerName)" 
             UpgradeCode="$(var.MyUpgradeCode)">

        <Package Compressed="yes" 
                 InstallScope="perMachine" 
                 Manufacturer="$(var.ManufacturerName)" 
                 Description="$(var.ProductDescription)" 
                 Comments="$(var.ProductCopyright)" />

        <MediaTemplate EmbedCab="yes" />

    </Product>

</Wix>

Implementing a Major Upgrade in your Setup

When creating an .msi-based installer, you are strongly encouraged to include logic that supports Windows Installer major upgrades. Major upgrades are the most common form of updates for .msi’s, and including support in your initial .msi release gives you flexibility in the future. Without including support for major upgrades you risk greatly complicating your distribution story if you ever need to release updates later on.

The steps you need to do:-

  1. You will need to set Id="*" (auto-generate) and define UpgradeCode="<GUID>" in your <Product> attributes.
  2. Add Upgrade element into your <Product> element.
  3. Then, schedule the removal of older version in InstallExecuteSequence element.
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">

    <!-- List of preprocessor variables -->

    <Product Id="$(var.MyProductCode)" 
             Name="$(var.MyProductName)"
             Language="1033" 
             Version="$(var.MyProductVersion)" 
             Manufacturer="$(var.ManufacturerName)" 
             UpgradeCode="$(var.MyUpgradeCode)">
        <Package Compressed="yes" 
                 InstallScope="perMachine" 
                 Manufacturer="$(var.ManufacturerName)" 
                 Description="$(var.ProductDescription)" 
                 Comments="$(var.ProductCopyright)" />
        <MediaTemplate EmbedCab="yes" />

        <!-- //Implement a Major Upgrade -->
        <Upgrade Id="$(var.MyUpgradeCode)">
            <UpgradeVersion Minimum="$(var.MyProductVersion)" OnlyDetect="yes" IncludeMinimum="yes" Property="DOWNGRADE_DETECTED" />
            <UpgradeVersion IncludeMinimum="no" Maximum="$(var.MyProductVersion)" IncludeMaximum="no" MigrateFeatures="yes" Property="UPGRADE_DETECTED" />
        </Upgrade>
        <InstallExecuteSequence>
            <RemoveExistingProducts After="InstallInitialize" />
        </InstallExecuteSequence>
        <Condition Message="A later version of [ProductName] is already installed. Setup will now exit.">NOT DOWNGRADE_DETECTED</Condition>

    </Product>

</Wix>

Installing program files and creating shortcuts

There are few things we need to do here for installing our files and shortcuts:-

  1. We need to create a target directory using Directory element.
  2. We need to create a reference to a particular target directory using DirectoryRef element.
  3. For each reference, we need to create one or more components using Component elements.
  4. We need to create one or more features using Feature element and associates particular components into a particular feature.
Source of file(s) to be installed:
--> "<WIX_PROJECT_DIR>\bin\ExampleApp.exe"

Installation target directory:
--> "C:\Program Files (x86)\Heiswayi Nrird\ExampleApp"

Application shortcuts to be created:
--> "Start Menu\ExampleApp\ExampleApp" (to launch ExampleApp.exe)
--> "Start Menu\ExampleApp\Uninstall ExampleApp" (to uninstall ExampleApp)

Target directories for our program files and our shortcuts

<Directory Id="TARGETDIR" Name="SourceDir">
    <Directory Id="ProgramFilesFolder">
        <Directory Id="MyProgramFiles" Name="$(var.ManufacturerName)">
            <Directory Id="APPLICATIONFOLDER" Name="$(var.MyProductName)"></Directory>
        </Directory>
        <Directory Id="ProgramMenuFolder" Name="$(var.ManufacturerName)">
            <Directory Id="ApplicationShortcutFolder" Name="$(var.MyProductName)"></Directory>
        </Directory>
    </Directory>
</Directory>

A directory reference for our shortcuts

<DirectoryRef Id="ApplicationShortcutFolder">
    <!-- Component(s) will be here -->
</DirectoryRef>

NOTE: A directory reference for our program file(s) will be automatically generated using Fragment element. Will be explained later.

Components for shortcuts

We have two components for our shortcuts; one if Windows architecture is 32-bit, another one if 64-bit. The reason for this is the location of msiexec.exe might be located in different system folder since we have a shortcut to uninstall our program using msiexec.exe. Only one of these two components will be used and it’s based on Condition element defined value in each component.

<DirectoryRef Id="ApplicationProgramsFolder">
    <Component Id="APPSHORTCUT64_comp" Guid="$(var.GuidProgramShortcut)">
        <Condition>VersionNT64</Condition>
        <Shortcut Id="ApplicationStartMenuShortcut" Name="$(var.MyProductName)" Description="$(var.ProductDescription)" Target="[APPLICATIONFOLDER]$(var.MainProgramFileFullname)" WorkingDirectory="APPLICATIONROOTDIRECTORY"/>
        <Shortcut Id="UninstallAppShortcut" Name="Uninstall $(var.MyProductName)" Target="[System64Folder]msiexec.exe" Arguments="/x [ProductCode]"/>
        <RemoveFolder Id="CleanUpShortCut" Directory="ApplicationProgramsFolder" On="uninstall"/>
        <RegistryValue Root="HKCU" Key="Software\$(var.ManufacturerName)\$(var.MyProductName)" Name="installed" Type="integer" Value="1" KeyPath="yes" />
    </Component>
    <Component Id="APPSHORTCUT_comp" Guid="$(var.GuidProgramShortcut)">
        <Condition>NOT VersionNT64</Condition>
        <Shortcut Id="ApplicationStartMenuShortcut" Name="$(var.MyProductName)" Description="$(var.ProductDescription)" Target="[APPLICATIONFOLDER]$(var.MainProgramFileFullname)" WorkingDirectory="APPLICATIONROOTDIRECTORY"/>
        <Shortcut Id="UninstallAppShortcut" Name="Uninstall $(var.MyProductName)" Target="[SystemFolder]msiexec.exe" Arguments="/x [ProductCode]"/>
        <RemoveFolder Id="CleanUpShortCut" Directory="ApplicationProgramsFolder" On="uninstall"/>
        <RegistryValue Root="HKCU" Key="Software\$(var.ManufacturerName)\$(var.MyProductName)" Name="installed" Type="integer" Value="1" KeyPath="yes" />
    </Component>
</DirectoryRef>

NOTE: Components for our program file(s) will be automatically generated using Fragment element. Will be explained later.

Features for our program files and our shortcuts

<Feature Id="ProgramFeature" Title="Program Files" Level="1">
    <ComponentGroupRef Id="APPLICATIONFOLDER_comp" />
</Feature>

<Feature Id="ShortcutFeature" Title="Shortcuts" Level="1">
    <ComponentRef Id="APPSHORTCUT64_comp" />
    <ComponentRef Id="APPSHORTCUT_comp" />
</Feature>

Generating component for each file to be installed automatically

Instead of manually creating one-by-one the component for each of our program files, we can use heat.exe tool along with candle.exe tool from WiX toolset to automatically harvest our program files and creates each component automatically.

Example commands:

"%WIX%bin\heat.exe" dir "bin" -cg APPLICATIONFOLDER_comp -gg -scom -sreg -sfrag -srd -dr APPLICATIONFOLDER -var var.ProjectDir -out "Product.Files.wxs"
"%WIX%bin\candle.exe" "Product.Files.wxs" -out "_Product.Files.wixobj" -dProjectDir=".\bin"

Those commands above will generate two files; Product.Files.wxs and _Product.Files.wixobj. Here is the content of Product.Files.wxs file:

<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
    <Fragment>
        <DirectoryRef Id="APPLICATIONFOLDER">
            <Component Id="cmp40DD4A73DF828C932073F22AA254E961" Guid="{746B1D16-E4BD-421B-BEAA-1F290EBB4276}">
                <File Id="fil3BA6726ABBBBF1BB587CA7587F5F7FA8" KeyPath="yes" Source="$(var.ProjectDir)\ExampleApp.exe" />
            </Component>
        </DirectoryRef>
    </Fragment>
    <Fragment>
        <ComponentGroup Id="APPLICATIONFOLDER_comp">
            <ComponentRef Id="cmp40DD4A73DF828C932073F22AA254E961" />
        </ComponentGroup>
    </Fragment>
</Wix>

Using built-in WixUI dialog sets

Since we have two features defined, and we want to let the user to choose which feature they want to install, so the simplest and best suited dialog set is WixUI_FeatureTree.

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">

    <!-- List of preprocessor variables -->

    <Product Id="$(var.MyProductCode)" 
             Name="$(var.MyProductName)"
             Language="1033" 
             Version="$(var.MyProductVersion)" 
             Manufacturer="$(var.ManufacturerName)" 
             UpgradeCode="$(var.MyUpgradeCode)">
        <Package Compressed="yes" 
                 InstallScope="perMachine" 
                 Manufacturer="$(var.ManufacturerName)" 
                 Description="$(var.ProductDescription)" 
                 Comments="$(var.ProductCopyright)" />
        <MediaTemplate EmbedCab="yes" />

        <!-- Implemention of Major Upgrade here -->

        <!-- Installation of files and shortcuts here -->

        <UIRef Id="WixUI_FeatureTree"/>

    </Product>

</Wix>

Example screenshot:

Simpler MSI setup

Installer graphics and icon

If you want to customize your MSI setup graphics instead of using the default reddish WiX graphics, you can add these two WiX variables under your <Product> element. Assuming all of your resource files are located under <WIX_PROJECT_DIR>\res folder. To get the dimension for WixUIBannerBmp and WixUIDialogBmp, you can check here.

<WixVariable Id="WixUIBannerBmp" Value="res\banner.bmp" />
<WixVariable Id="WixUIDialogBmp" Value="res\dialog.bmp" />

Here’s for the MSI setup icon, similarly it goes under your <Product> element:

<Icon Id="icon.ico" SourceFile="res\appIcon.ico"/>
<Property Id="ARPPRODUCTICON" Value="icon.ico" />

License file

When you use the built-in WixUI dialog set, you need to define your license file. Simply get your License.rtf ready and add this under your <Product> element. Assuming your license file is located under <WIX_PROJECT_DIR>\doc folder.

<WixVariable Id="WixUILicenseRtf" Value="doc\License.rtf" />

Detecting a required minimum version of .NET Framework (Optional)

If your program requires a certain minimum version of .NET Framework needs to be installed in order to work, you can include a detection using <Condition> element to check and then inform the user that they need a particular version of .NET Framework to be installed before they can install and use your program files. Let’s say your program requires .NET Framework 4.7.1, here what you need to do:

First, you need to include a reference xmlns:netfx="... into your WiX file:

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" 
     xmlns:netfx="http://schemas.microsoft.com/wix/NetFxExtension">

    <Product>
        <!-- Your MSI setup design here -->
    </Product>

</Wix>

And then, include this Condition under your <Product> element:

<PropertyRef Id="NETFRAMEWORK45" />
<Condition Message="[ProductName] requires .NET Framework 4.7.1. Please install the .NET Framework then run this installer again. Setup will now exit.">
    <![CDATA[Installed OR (NETFRAMEWORK45 AND NETFRAMEWORK45 >= "#461308")]]>
</Condition>

Please note that #461308 represents a .NET Framework version for 4.7.1. You can check here for more details about .NET Framework versions.

Example screenshot:

Required .NET Framework 4.7.1 to be installed

Commands to build your MSI setup

I’m using a batch scripting and these are the commands I use to build my MSI setup output:

@echo off
set projectDir=.\bin

rem Automatically harvest program files and create each component
"%WIX%bin\heat.exe" dir "bin" -cg APPLICATIONFOLDER_comp -gg -scom -sreg -sfrag -srd -dr APPLICATIONFOLDER -var var.ProjectDir -out "Product.Files.wxs"
"%WIX%bin\candle.exe" "Product.Files.wxs" -out "_Product.Files.wixobj" -dProjectDir="%projectDir%"

rem Compile and create mySetup.msi
"%WIX%bin\candle.exe" "Product.wxs" -out "_Product.wixobj" -nologo
"%WIX%bin\light.exe" "_Product.Files.wixobj" "_Product.wixobj" -cultures:en-US -ext WixUIExtension -ext WixNetFxExtension -out "mySetup.msi" -nologo

Please note that if you’re using built-in WixUI dialog set, you need to include -cultures:en-US -ext WixUIExtension into your light.exe commands as shown above. For .NET Framework detection, you need to append -ext WixNetFxExtension into the commands.

Full complete source code

Product.wxs

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:netfx="http://schemas.microsoft.com/wix/NetFxExtension">
    <?define MyProductName = "ExampleApp" ?>
    <?define MyProductVersion = "2.0.0" ?>
    <?define MyProductCode = "*" ?>
    <?define MyUpgradeCode = "DA3EEBC7-5812-4380-BDAE-1605417230CB" ?>
    <?define ManufacturerName = "Heiswayi Nrird" ?>
    <?define ProductDescription = "Demo installer for WiX basics" ?>
    <?define ProductCopyright = "(c) 2018 $(var.ManufacturerName)" ?>
    <?define MainProgramFileFullname = "ExampleApp.exe" ?>
    <?define GuidProgramShortcut = "9ACF08F5-CFF4-43C1-84F9-418ADAAD772D" ?>
    <?define GuidProgramShortcut64 = "CEA4CD03-0819-4A30-903A-22E09B254F04" ?>
    <?define LicenseEulaFile = "License.rtf" ?>
    <?define BannerBmp = "banner.bmp" ?>
    <?define DialogBmp = "dialog.bmp" ?>
    <?define AppIcon = "appIcon.ico" ?>

    <Product Id="$(var.MyProductCode)" Name="$(var.MyProductName)" Language="1033" Version="$(var.MyProductVersion)" Manufacturer="$(var.ManufacturerName)" UpgradeCode="$(var.MyUpgradeCode)">
        <Package Compressed="yes" InstallScope="perMachine" Manufacturer="$(var.ManufacturerName)" Description="$(var.ProductDescription)" Comments="$(var.ProductCopyright)" />
        <MediaTemplate EmbedCab="yes" />

        <WixVariable Id="WixUILicenseRtf" Value="$(var.LicenseEulaFile)" />
        <WixVariable Id="WixUIBannerBmp" Value="$(var.BannerBmp)" />
        <WixVariable Id="WixUIDialogBmp" Value="$(var.DialogBmp)" />

        <Icon Id="icon.ico" SourceFile="$(var.AppIcon)"/>
        <Property Id="ARPPRODUCTICON" Value="icon.ico" />

        <Upgrade Id="$(var.MyUpgradeCode)">
            <UpgradeVersion Minimum="$(var.MyProductVersion)" OnlyDetect="yes" IncludeMinimum="yes" Property="DOWNGRADE_DETECTED" />
            <UpgradeVersion IncludeMinimum="no" Maximum="$(var.MyProductVersion)" IncludeMaximum="no" MigrateFeatures="yes" Property="UPGRADE_DETECTED" />
        </Upgrade>
        <InstallExecuteSequence>
            <RemoveExistingProducts After="InstallInitialize" />
        </InstallExecuteSequence>
        <Condition Message="A later version of [ProductName] is already installed. Setup will now exit.">NOT DOWNGRADE_DETECTED</Condition>

        <PropertyRef Id="NETFRAMEWORK45" />
        <Condition Message="[ProductName] requires .NET Framework 4.7.1. Please install the .NET Framework then run this installer again. Setup will now exit.">
            <![CDATA[Installed OR (NETFRAMEWORK45 AND NETFRAMEWORK45 >= "#461308")]]>
        </Condition>

        <Directory Id="TARGETDIR" Name="SourceDir">
            <Directory Id="ProgramFilesFolder">
                <Directory Id="MyProgramFiles" Name="$(var.ManufacturerName)">
                    <Directory Id="APPLICATIONFOLDER" Name="$(var.MyProductName)"></Directory>
                </Directory>
                <Directory Id="ProgramMenuFolder" Name="$(var.ManufacturerName)">
                    <Directory Id="ApplicationShortcutFolder" Name="$(var.MyProductName)"></Directory>
                </Directory>
            </Directory>
        </Directory>

        <DirectoryRef Id="ApplicationShortcutFolder">
            <Component Id="APPSHORTCUT64_comp" Guid="$(var.GuidProgramShortcut64)">
                <Condition>VersionNT64</Condition>
                <Shortcut Id="ApplicationStartMenuShortcut64" Name="$(var.MyProductName)" Description="$(var.ProductDescription)" Target="[APPLICATIONFOLDER]$(var.MainProgramFileFullname)" WorkingDirectory="APPLICATIONROOTDIRECTORY"/>
                <Shortcut Id="UninstallAppShortcut64" Name="Uninstall $(var.MyProductName)" Target="[System64Folder]msiexec.exe" Arguments="/x [ProductCode]"/>
                <RemoveFolder Id="CleanUpShortCut64" Directory="ApplicationShortcutFolder" On="uninstall"/>
                <RegistryValue Root="HKCU" Key="Software\$(var.ManufacturerName)\$(var.MyProductName)" Name="installed" Type="integer" Value="1" KeyPath="yes" />
            </Component>
            <Component Id="APPSHORTCUT_comp" Guid="$(var.GuidProgramShortcut)">
                <Condition>NOT VersionNT64</Condition>
                <Shortcut Id="ApplicationStartMenuShortcut" Name="$(var.MyProductName)" Description="$(var.ProductDescription)" Target="[APPLICATIONFOLDER]$(var.MainProgramFileFullname)" WorkingDirectory="APPLICATIONROOTDIRECTORY"/>
                <Shortcut Id="UninstallAppShortcut" Name="Uninstall $(var.MyProductName)" Target="[SystemFolder]msiexec.exe" Arguments="/x [ProductCode]"/>
                <RemoveFolder Id="CleanUpShortCut" Directory="ApplicationShortcutFolder" On="uninstall"/>
                <RegistryValue Root="HKCU" Key="Software\$(var.ManufacturerName)\$(var.MyProductName)" Name="installed" Type="integer" Value="1" KeyPath="yes" />
            </Component>
        </DirectoryRef>

        <Feature Id="ProgramFeature" Title="Program Files" Level="1">
            <ComponentGroupRef Id="APPLICATIONFOLDER_comp" />
        </Feature>
        <Feature Id="ShortcutFeature" Title="Shortcuts" Level="1">
            <ComponentRef Id="APPSHORTCUT64_comp" />
            <ComponentRef Id="APPSHORTCUT_comp" />
        </Feature>

        <UIRef Id="WixUI_FeatureTree"/>
    </Product>
</Wix>