From MSI to WiX, Part 8 - Major Upgrade

  • The main page for the series is here.

 

Introduction

A typical Major Upgrade removes a previous version of an application and installs a new version.  This blog will guide you through the process of creating Major Upgrade.

Source code for RTM version

Let's create a C# solution with two projects in it: library and console application.  Here is the code for the library:

using System;

using System.Collections.Generic;

using System.Text;

namespace TestLib

{

    public class TestClass

    {

        private string greeting;

        public TestClass()

        {

            greeting = "Hello, World!";

        }

        public TestClass(string greeting)

        {

            this.greeting = greeting;

        }

        public void WriteLine()

        {

            Console.WriteLine(greeting);

        }

    }

}

Here is the code for the console application:

using System;

using System.Collections.Generic;

using System.Text;

using TestLib;

[assembly: AssemblyVersion("1.0.0.0")]

[assembly: AssemblyFileVersion("1.0.0.0")]

namespace

TestApp

{

    class Program

    {

        static void Main(string[] args)

        {

            TestClass test = new TestClass();

            test.WriteLine();

        }

    }

}

As you can see, console application is using default constructor, so calling WriteLine method of the TestClass class will print out "Hello, World!" message.

Source code for V2 version

For version 2 of our product we will change the console application to use a non-default constructor to pass the greeting string the the TestClass instance:

using System;

using System.Collections.Generic;

using System.Text;

using TestLib;

[assembly: AssemblyVersion("1.0.1.0")]

[assembly: AssemblyFileVersion("1.0.1.0")]

namespace

TestApp

{

    class Program

    {

        static void Main(string[] args)

        {

            TestClass test = new TestClass("QFE version");

            test.WriteLine();

        }

    }

}

Our updated version of application will print out "QFE version" message instead of default "Hello, World!".  Notice also that AssemblyVersion and AssemblyFileversion have changed.

Installation Wix script with no Major Upgrade support

Typical installation script for such program would be something like this:

<?xml version='1.0' encoding='windows-1252'?>

<Wix xmlns='https://schemas.microsoft.com/wix/2003/01/wi'>

  <?define SkuName = "TestApp"?>

  <?define ProductVersion="1.0.0" ?>

  <?define UpgradeCode="{3485E6A2-A1F3-4329-8BB5-ED8FFCF283D4}"?>

  <?define Manufacturer="Acme Corp."?>

  <?define ProductCode="{5C32A3BD-3BA3-43AF-951F-1077E84B00DC}"?>

  <?define PackageCode="{????????-????-????-????-????????????}"?>

  <Product Id='$(var.ProductCode)'

           Name='ProductName'

           Language='1033'

           Version='$(var.ProductVersion)'

           Codepage='1252'

           Manufacturer='$(var.Manufacturer)'

           UpgradeCode='$(var.UpgradeCode)'>

    <Package Id='$(var.PackageCode)'

             Description="PackageDescription"

             Comments='Comments'

             Manufacturer='$(var.Manufacturer)'

             InstallerVersion='200'

             Languages='1033'

             SummaryCodepage='1252'

             Compressed='yes'

             AdminImage='no'

             Platforms='Intel'

             ReadOnly='yes'

             ShortNames='no'

             Keywords='Installer,MSI,Database' />

<Media Id="1" Cabinet="$(var.SkuName).cab" EmbedCab="yes" />

 

    <Directory Id='TARGETDIR' Name='SourceDir'>

      <Directory Id='LocalAppDataFolder'>

        <Directory Id='INSTALLDIR' Name='TestApp'>

          <Component Id='$(var.SkuName)' Guid='{835A4136-B01E-4F8B-8EA7-5D6F69B07A83}'>

            <File Id='TestAppExe' DiskId='1' KeyPath='yes' Checksum='yes'

                 Vital='yes' Name='TestApp.exe' Assembly='.net'

                 AssemblyManifest='TestAppExe'

                 AssemblyApplication='TestAppExe'

                  Source='TestApp.exe' />

          </Component>

          <Component Id='TestLibDll_Component' Guid='{5BC55186-170E-475C-B77A-D80581FC88EC}'>

            <File Id='TestLibDll' Name='TestLib.dll' DiskId='1' KeyPath='yes'

                 Vital='yes' Assembly='.net' AssemblyManifest='TestLibDll'

                 AssemblyApplication='TestLibDll' Source='TestLib.dll' />

          </Component>

        </Directory>

      </Directory>

    </Directory>

    <Feature Id='Complete' Level='1'>

      <ComponentRef Id='$(var.SkuName)' />

      <ComponentRef Id='TestLibDll_Component' />

    </Feature>

  </Product>

</Wix>

Aside from UpgradeCode that script does not have any support for major upgrades.

Preparing an application for future major upgrades

Here is what needs to be done from purely MSI standpoint in order to support major upgrades:

  • Add a record to the Upgrade table to detect if installed on the system application has older version than current installation package.  We need this in case we need to apply conditions for upgrade only.  For example, we may update some configuration file during regular install and skip updating during upgrade thus preserving changes users might add to the configuration file after the original install.  This record should have the following values for columns:
    • UpgradeCode is set to the UpgradeCode property value
    • VersionMin is set to RTM version of the product
    • VersionMax is set to the current product version
    • Language can be set to a product language or empty
    • Attributes is set to msidbUpgradeAttributesVersionMinInclusive
    • Remove is empty
    • ActionProperty is set to the name of the public property which FindRelatedProducts action will set to the product code of already installed related product.  Suggested name for this property is UPGRADEFOUND.
  • Add a record to the Upgrade table to detect if current installation package contains an older version of an application.  We need this to prevent downgrading installed application to older version.  This record should have the following values for columns:
    • UpgradeCode is set to the UpgradeCode property value
    • VersionMin is set to current product version
    • VersionMax is empty
    • Language can be set to a product language or empty
    • Attributes is set to msidbUpgradeAttributesOnlyDetect
    • Remove is empty
    • ActionProperty is set to the name of the public property which FindRelatedProducts action will set to the product code of already installed related product.  Suggested name for this property is NEWPRODUCTFOUND.
  • FindRelatedProducts action should be scheduled in both InstallExecuteSequence and InstallUISequence tables.
  • Custom action Type 19 should be scheduled in both InstallExecuteSequence and InstallUISequence tables.  This action should be conditioned on NEWPRODUCTFOUND property.
  • MigrateFeatureStates action should be scheduled in both InstallExecuteSequence and InstallUISequence tables.  This action sets the feature states on installed application to reflect the feature states of already installed application.
  • RemoveExistingProducts action should be scheduled in InstallExecuteSequence table. This action removes the installed product or only updated files depending on where this action is scheduled in the InstallExecuteSequence table.
  • Both UPGRADEFOUND and NEWPRODUCTFOUND public properties  must be added to the SecureCustomProperties property.
  • Change ProductCode, PackageCode, and ProductVersion properties.
  • Important:   Product language defined in the Language attribute of <Product> element must be one of the languages listed in the Languages attribute of the <Package> element.

Here is how these items translates to Wix:

<Upgrade Id="$(var.UpgradeCode)">

  <UpgradeVersion Minimum="$(var.ProductVersion)"

                  IncludeMinimum="no"

                  OnlyDetect="yes"

Language="1033"

Property="NEWPRODUCTFOUND" />

  <UpgradeVersion Minimum="$(var.RTMProductVersion)"

                  IncludeMinimum="yes"

                  Maximum="$(var.ProductVersion)"

                  IncludeMaximum="no"

Language="1033"

Property="UPGRADEFOUND" />

</Upgrade>

Id attribute of the <Upgrade> element is set to a value of UpgradeCode property (UpgradeCode attribute of <Product> element) and will be added to the UpgradeCode column of every record in the Upgrade table.

Every <UpgradeVersion> element adds a new record to the Upgrade table.

Minimum attribute of the <UpgradeVersion> element sets the value of VersionMin column.

Maximum attribute of the <UpgradeVersion> element sets the value of VersionMax column.

Property attribute of the <UpgradeVersion> element sets the value of ActionProperty column.

Here is the relationships between flag bits in Attributes column of the Upgrade table and attributes of the <UpgradeVersion> element:

Flag Attribute Description
msidbUpgradeAttributesMigrateFeatures MigrateFeatures Enables the logic of MigrateFeatureStates action of migrating feature states.
msidbUpgradeAttributesOnlyDetect OnlyDetect Detects products and applications but does not remove.
msidbUpgradeAttributesIgnoreRemoveFailure IgnoreremoveFailure Continues installation upon failure to remove a product or application.
msidbUpgradeAttributesVersionMinInclusive IncludeMinimum Detects the range of versions including the value in VersionMin.
msidbUpgradeAttributesVersionMaxInclusive IncludeMaximum Detects the range of versions including the value in VersionMax.
msidbUpgradeAttributesLanguagesExclusive ExcludeLanguages Detects all languages, excluding the languages listed in the Language column.

<!-- Prevent downgrading -->

<CustomAction Id="PreventDowngrading"

             Error="Newer version already installed." />

<InstallExecuteSequence>

  <Custom Action="PreventDowngrading"

         After="FindRelatedProducts">NEWPRODUCTFOUND</Custom>

  <RemoveExistingProducts After="InstallFinalize" />

</InstallExecuteSequence>

<InstallUISequence>

  <Custom Action="PreventDowngrading"

         After="FindRelatedProducts">NEWPRODUCTFOUND</Custom>

</InstallUISequence>

Here is an updated installation script:

<?xml version='1.0' encoding='windows-1252'?>

<Wix xmlns='https://schemas.microsoft.com/wix/2003/01/wi'>

  <?define SkuName = "TestApp"?>

<?

define RTMProductVersion="1.0.0" ?>

<?define ProductVersion="2.0.0" ?>

  <?define UpgradeCode="{3485E6A2-A1F3-4329-8BB5-ED8FFCF283D4}"?>

  <?define Manufacturer="Acme Corp."?>

  <?define PackageCode="{????????-????-????-????-????????????}"?>

<Product Id='{8EEB7D19-F7F4-4218-93B9-BBEAAA4C2E2D}'

           Name='ProductName'

           Language='1033'

           Version='$(var.ProductVersion)'

           Codepage='1252'

           Manufacturer='$(var.Manufacturer)'

           UpgradeCode='$(var.UpgradeCode)'>

    <Package Id='$(var.PackageCode)'

             Description="PackageDescription"

             Comments='Comments'

             Manufacturer='$(var.Manufacturer)'

             InstallerVersion='200'

             Languages='1033'

             SummaryCodepage='1252'

             Compressed='yes'

             AdminImage='no'

             Platforms='Intel'

             ReadOnly='yes'

             ShortNames='no'

             Keywords='Installer,MSI,Database' />

<Media Id="1" Cabinet="$(var.SkuName).cab" EmbedCab="yes" />

 

    <Upgrade Id="$(var.UpgradeCode)">

      <UpgradeVersion Minimum="$(var.ProductVersion)"

                      IncludeMinimum="no"

                      OnlyDetect="yes"

Language="1033"

Property="NEWPRODUCTFOUND" />

      <UpgradeVersion Minimum="$(var.RTMProductVersion)"

                      IncludeMinimum="yes"

                      Maximum="$(var.ProductVersion)"

                      IncludeMaximum="no"

Language="1033"

Property="UPGRADEFOUND" />

    </Upgrade>

 

    <Directory Id='TARGETDIR' Name='SourceDir'>

      <Directory Id='LocalAppDataFolder'>

        <Directory Id='INSTALLDIR' Name='TestApp'>

          <Component Id='$(var.SkuName)' Guid='{835A4136-B01E-4F8B-8EA7-5D6F69B07A83}'>

            <File Id='TestAppExe' DiskId='1' KeyPath='yes' Checksum='yes'

                 Vital='yes' Name='TestApp.exe' Assembly='.net'

                 AssemblyManifest='TestAppExe'

                 AssemblyApplication='TestAppExe'

                  Source='TestApp.exe' />

          </Component>

          <Component Id='TestLibDll_Component' Guid='{5BC55186-170E-475C-B77A-D80581FC88EC}'>

            <File Id='TestLibDll' Name='TestLib.dll' DiskId='1' KeyPath='yes'

                 Vital='yes' Assembly='.net' AssemblyManifest='TestLibDll'

                 AssemblyApplication='TestLibDll' Source='TestLib.dll' />

          </Component>

        </Directory>

      </Directory>

    </Directory>

    <Feature Id='Complete' Level='1'>

      <ComponentRef Id='$(var.SkuName)' />

      <ComponentRef Id='TestLibDll_Component' />

    </Feature>

 

<!-- Prevent downgrading -->

<CustomAction Id="PreventDowngrading" Error="Newer version already installed." />

<!-- Sequences -->

<InstallExecuteSequence>

<Custom Action="PreventDowngrading" After="FindRelatedProducts">NEWPRODUCTFOUND</Custom>

<RemoveExistingProducts After="InstallFinalize" />

</InstallExecuteSequence>

<InstallUISequence>

<Custom Action="PreventDowngrading" After="FindRelatedProducts">NEWPRODUCTFOUND</Custom>

</InstallUISequence>

  </Product>

</Wix>

How upgrade works

  • The FindRelatedProducts action runs through each record of the Upgrade table in sequence and compares the upgrade code, product version, and language in each row to products installed on the system. When FindRelatedProducts detects a correspondence between the upgrade information and an installed product, it appends the product code to the property specified in the ActionProperty column of the Upgrade table (Property attribute of the <UpgradeVersion> element).

For FindRelatedProducts to work correctly, the package author must be sure that the ProductLanguage property in the Property table is set to a language that is also listed in the Template Summary Property.

FindRelatedProducts should be authored into the InstallUISequence table and InstallExecuteSequence tables.  The FindRelatedProducts action must come before the MigrateFeatureStates action and the RemoveExistingProducts action.

  • MigrateFeatureStates reads the feature states in the existing application and then sets these feature states in the pending installation. The method is only useful when the new feature tree has not greatly changed from the original.

MigrateFeatureStates action runs through each record of the Upgrade table in sequence and compares the upgrade code, product version, and language in each row to all products installed on the system. If MigrateFeatureStates action detects a correspondence, and if the msidbUpgradeAttributesMigrateFeatures bit flag is set in the Attributes column of the Upgrade table, the installer queries the existing feature states for the product and sets these states for the same features in the new application. The action only migrates the feature states if the Preselected property is not set.

The MigrateFeatureStates action should come immediately after the CostFinalize action. MigrateFeatureStates must be sequenced in both the InstallUISequence table and the InstallExecuteSequence table.

  • The RemoveExistingProducts action goes through the product codes listed in the ActionProperty column of the Upgrade table and removes the products in sequence by invoking concurrent installations. For each concurrent installation the installer sets the ProductCode property to the product code and sets the REMOVE property to the value in the Remove field of the Upgrade table. If the Remove field is blank, its value defaults to ALL and the installer removes the entire product.

The RemoveExistingProducts action must be scheduled in the action sequence in one of the following locations.

    • Between the InstallValidate action and the InstallInitialize action. In this case, the installer removes the old applications entirely before installing the new applications. This is an inefficient placement for the action because all reused files have to be recopied.
    • After the InstallInitialize action and before any actions that generate execution script.
    • Between the InstallExecute action, or the InstallExecuteAgain action, and the InstallFinalize action. Generally the last three actions are scheduled right after one another: InstallExecute, RemoveExistingProducts, and InstallFinalize. In this case the updated files are installed first and then the old files are removed. However, if the removal of the old application fails, then the installer rolls back both the removal of the old application and the install of the new application.
    • After the InstallFinalize action. This is the most efficient placement for the action. In this case, the installer updates files before removing the old applications. Only the files being updated get installed during the installation. If the removal of the old application fails, then the installer only rolls back the uninstallation of the old application.

Windows Installer sets the UPGRADINGPRODUCTCODE Property when it runs this action.