From MSI to WiX, Part 9 - Patching

The main page for the series is here.

 

Introduction

I'll show a very simple sample of small update.

 

Directory structure for the sample

The directory structure I am using for this sample is:

Patching

    SmallUpdate

    RTM

    QFE

    Include

        Patch

    Project

        TestApp

            TestApp

            TestLib

        TestApp2

            TestApp

            TestLib

SmallUpdate folder contains the WiX source code and batch files for building and installing the installer databases.  RTM contains the first version of the product and QFE - updated version.  Include folder contains all files shared by both RTM and QFE.  Patch folder contains the patch building WiX source.

Test application source and compiled code is stored under the Project folder.  TestApp is the original version, and TestApp2 - is updated version.

Source code for original version of the product (RTM)

Our product will consist of one Windows Console application (TestApp folder) and one dll (TestLib folder).

Here is the code for the console application:

Program.cs:

using System;

using System.Collections.Generic;

using System.Text;

using TestLib;

namespace TestApp

{

    class Program

    {

        static void Main(string[] args)

        {

            TestClass test = new TestClass();

            test.WriteLine();

        }

    }

}

AssemblyInfo.cs:

using System.Reflection;

using System.Runtime.CompilerServices;

using System.Runtime.InteropServices;

// General Information about an assembly is controlled through the following

// set of attributes. Change these attribute values to modify the information

// associated with an assembly.

[assembly: AssemblyTitle("TestApp")]

[assembly: AssemblyDescription("")]

[assembly: AssemblyConfiguration("")]

[assembly: AssemblyCompany("")]

[assembly: AssemblyProduct("TestApp")]

[assembly: AssemblyCopyright("Copyright © 2008")]

[assembly: AssemblyTrademark("")]

[assembly: AssemblyCulture("")]

// Setting ComVisible to false makes the types in this assembly not visible

// to COM components. If you need to access a type in this assembly from

// COM, set the ComVisible attribute to true on that type.

[assembly: ComVisible(false)]

// The following GUID is for the ID of the typelib if this project is exposed to COM

[assembly: Guid("10578ea5-d18c-41b1-bbf6-ccc9d4d695f2")]

// Version information for an assembly consists of the following four values:

//

// Major Version

// Minor Version

// Build Number

// Revision

//

[assembly: AssemblyVersion("1.0.0.0")]

[assembly: AssemblyFileVersion("1.0.0.0")]

Source code for the dll:

TestClass.cs:

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);

        }

    }

}

AssemblyInfo.cs:

using System.Reflection;

using System.Runtime.CompilerServices;

using System.Runtime.InteropServices;

// General Information about an assembly is controlled through the following

// set of attributes. Change these attribute values to modify the information

// associated with an assembly.

[assembly: AssemblyTitle("TestLib")]

[assembly: AssemblyDescription("")]

[assembly: AssemblyConfiguration("")]

[assembly: AssemblyCompany("")]

[assembly: AssemblyProduct("TestLib")]

[assembly: AssemblyCopyright("Copyright © 2008")]

[assembly: AssemblyTrademark("")]

[assembly: AssemblyCulture("")]

// Setting ComVisible to false makes the types in this assembly not visible

// to COM components. If you need to access a type in this assembly from

// COM, set the ComVisible attribute to true on that type.

[assembly: ComVisible(false)]

// The following GUID is for the ID of the typelib if this project is exposed to COM

[assembly: Guid("5b0112f9-befc-4957-99f8-8ef51d20d3cf")]

// Version information for an assembly consists of the following four values:

//

// Major Version

// Minor Version

// Build Number

// Revision

//

// You can specify all the values or you can default the Revision and Build Numbers

// by using the '*' as shown below:

[assembly: AssemblyVersion("1.0.0.0")]

[assembly: AssemblyFileVersion("1.0.0.0")]

Our TestClass from the dll has two constructors: default constructor sets the value of the greeting varibale to it's default value, but users are free to use the second constructor to set the greeting message to whatever they like.

Console application is using the default constructor, so the message we will see on the screen is the default message of the TestClass class.

WiX shared code

Definitions.wxi:

<Include>

  <!-- Package all files in one cab file -->

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

  <!-- Common conditions -->

  <!-- .Net Framework 2.0 is required -->

  <Condition Message='.NET Framework 2.0 must be installed prior to installation of this product.'>

    Installed OR MsiNetAssemblySupport >= "2.0.50727"

  </Condition>

</Include>

Here I have the declaration of the Media element and one Launch Condition demanding that .NET Framework 2.0 must be installed on the target system.

UpgradeData.wxi:

<Include>

  <!-- Never change the UpgradeCode -->

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

  <?define Manufacturer="Microsoft"?>

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

 

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

  <?define ProductVersion="1.0.0" ?>

</Include>

Here are the values for properties related to upgrade.

en-us.wxl:

<?xml version="1.0" encoding="utf-8" ?>

<WixLocalization>

  <String Id="LangId">1033</String>

  <String Id="Codepage">1252</String>

  <String Id="PackageDescription">TestApp application</String>

  <String Id="ProductName">TestApp</String>

  <String Id="Comments">TestApp application sample to demonstrate the QFE patching</String>

</WixLocalization>

Here I have localized values for installation package properties.

WiX source code for the installer for original version of our product

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

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

  <?define SkuName = "TestApp"?>

  <!-- Path variables -->

  <?define APPROOT = "D:\MyLearning\WiX\Patching"?>

  <?define PROJECT = "$(var.APPROOT)\Project\TestApp\TestApp\bin\Debug"?>

  <?include ..\Include\UpgradeData.wxi ?>

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

           Name='$(loc.ProductName)'

           Language='$(loc.LangId)'

           Version='$(var.ProductVersion)'

           Codepage='1252'

           Manufacturer='$(var.Manufacturer)'

           UpgradeCode='$(var.UpgradeCode)'>

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

             Description="$(loc.PackageDescription)"

             Comments='$(loc.Comments)'

             Manufacturer='$(var.Manufacturer)'

             InstallerVersion='200'

             Languages='$(loc.LangId)'

             SummaryCodepage='$(loc.Codepage)'

             Compressed='no'

             AdminImage='no'

             Platforms='Intel'

             ReadOnly='yes'

             ShortNames='no'

             Keywords='Installer,MSI,Database' />

    <?include ..\Include\Definitions.wxi ?>

    <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='$(var.PROJECT)\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='$(var.PROJECT)\TestLib.dll' />

          </Component>

        </Directory>

      </Directory>

    </Directory>

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

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

      <ComponentRef Id='TestLibDll_Component' />

    </Feature>

  </Product>

</Wix>

There is nothing special in here except one attribute:

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

In order to create a patch we must have uncompressed source.  One way of getting it is to create an administartive install and create a patch out of an administrative image.  For this sample I am setting the Compressed attribute to no, which means that by default, none of the files will be embedded into the installer database.  That can be overridden on the file by file basis by adding Compressed attribute to the File element, but I am not going to do that in this sample.

Here are commands to build the installer database:

d:\Wix\candle.exe RTM.wxs

d:\Wix\light.exe -out TestApp.msi RTM.wixobj -loc ..\Include\en-us.wxl

Updated application source code (QFE)

Program.cs:

using System;

using System.Collections.Generic;

using System.Text;

using TestLib;

namespace TestApp

{

    class Program

    {

        static void Main(string[] args)

        {

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

            test.WriteLine();

        }

    }

}

Notice that now I am using non-default constructor providing my custom greetings message.

AssemblyInfo.cs:

using System.Reflection;

using System.Runtime.CompilerServices;

using System.Runtime.InteropServices;

// General Information about an assembly is controlled through the following

// set of attributes. Change these attribute values to modify the information

// associated with an assembly.

[assembly: AssemblyTitle("TestApp")]

[assembly: AssemblyDescription("")]

[assembly: AssemblyConfiguration("")]

[assembly: AssemblyCompany("")]

[assembly: AssemblyProduct("TestApp")]

[assembly: AssemblyCopyright("Copyright © 2008")]

[assembly: AssemblyTrademark("")]

[assembly: AssemblyCulture("")]

// Setting ComVisible to false makes the types in this assembly not visible

// to COM components. If you need to access a type in this assembly from

// COM, set the ComVisible attribute to true on that type.

[assembly: ComVisible(false)]

// The following GUID is for the ID of the typelib if this project is exposed to COM

[assembly: Guid("10578ea5-d18c-41b1-bbf6-ccc9d4d695f2")]

// Version information for an assembly consists of the following four values:

//

// Major Version

// Minor Version

// Build Number

// Revision

//

[assembly: AssemblyVersion("1.0.1.0")]

[assembly: AssemblyFileVersion("1.0.1.0")]

Notice that both AssemblyVersion and AssemblyFileVersion attributes have changed their values from "1.0.0.0" to "1.0.1.0".

Thare are no changes in the dll source code.

WiX source code for the installer for updated version of our product

There are no changes to the original WiX source code except this one:

<?define PROJECT = "$(var.APPROOT)\Project\TestApp2\TestApp\bin\Debug"?>

That changes the path to the updated version.

Because this is a small update, the only property we need to change is the Revision Number Summary property (or, PackageCode, or, in WiX, the Id attribute of the Package element). 

Now we have both original (RTM) and updated (QFE) versions of installer databases built.  It is time to create a patch creation script.

Patch creation script (Patch.wxs)

Here is the source of Patch.wxs.  This file is located in the Patch folder.

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

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

  <?define SkuName = "TestApp"?>

  <!-- Path variables -->

  <?define APPROOT = "D:\MyLearning\WiX\Patching\SmallUpdate"?>

  <?define RTM = "$(var.APPROOT)\RTM"?>

  <?define QFE = "$(var.APPROOT)\QFE"?>

  <?include ..\Include\UpgradeData.wxi ?>

  <PatchCreation Id='{F8D2A922-EFE7-4BBF-8942-73A487453C2F}'

                 AllowMajorVersionMismatches='no'

                 AllowProductCodeMismatches='no'

                 CleanWorkingFolder='no'

                 WholeFilesOnly='no'

                 Codepage='1252' >

    <PatchInformation Description="TestApp 1.0.0 Patch"

                      Keywords='Installer'

                      Comments='testApp comments'

                      Manufacturer='$(var.Manufacturer)'

                      Languages='1033'

                      Compressed='yes'

                      SummaryCodepage='1252' />

    <PatchMetadata Description="TestApp 1.0.0 Patch"

                   DisplayName='TestApp 1.0.0 Patch'

                   TargetProductName='TestApp 1.0.0'

                   ManufacturerName='$(var.Manufacturer)'

                   MoreInfoURL='www.acme.com'

                   Classification='Hotfix'

                   AllowRemoval='yes' />

    <Family Name='Patch101' DiskId='2' MediaSrcProp='PatchSrcPropName' SequenceStart='1000'>

      <UpgradeImage Id='PatchQFE' SourceFile='$(var.QFE)\TestApp.msi'>

        <TargetImage Id='PatchRTM' Order='1' IgnoreMissingFiles='no' SourceFile='$(var.RTM)\TestApp.msi' />

      </UpgradeImage>

    </Family>

    <TargetProductCode Id='$(var.ProductCode)' />

  </PatchCreation>

</Wix>

Use the following commands to build the Patch.msp:

d:\wix\candle.exe Patch.wxs

d:\wix\light.exe Patch.wixobj

Msimsp.exe -s Patch.pcp -p Patch.msp -l Patch.log

Now, install the original version of the product.  Run the installed application.  On my computer it is installed in C:\Users\alexshev\AppData\Local\TestApp folder.  You should see the "Hello, World!" mesage.

Install the patch by running this command from the Patch folder:

msiexec /p Patch.msp REINSTALL=ALL REINSTALLMODE=omus

Run TestApp.exe again.  This time it should print "QFE version" message.

Open Add/Remove Programs in the Control Panel applet.  Turn on the view of the installed updates for the installed products.  Find the TestApp application in the list of installed applications.  You should see one update named "TestApp 1.0.0 Patch".  Right-click on it and select "Uninstall".  Continue with uninstallation.  Run TestApp.exe again.  You will see "Hello, World!" message again.