« »

building MSI patch packages (.msp) with WiX

Tuesday, 29 May 2007

This post includes a complete and concrete example of building an MSI patch package (a .msp file to upgrade an existing .msi installation) with WiX.

Background

I'm responsible for building the ActivePython and Komodo installers at ActiveState. On Windows we build MSI packages for installation.

Currently I'm investigating auto-update support for Komodo 4.2. Because Komodo is based on Firefox/Mozilla we can benefit from the excellent Mozilla update system (I'll write another post about our experience with it). However, integrating with an MSI-based installation isn't something the Mozilla update system does out of the box: Firefox and Thunderbird don't use MSI for their installers (they use NSIS), hence I suspect working with MSI was never a design consideration.

While working out how to best marry MSI and Moz update, I investigated producing MSI patch packages (.msp files) for Komodo updates. MSI is a complex and complicated technology (it would be nice if the latter, at least, wasn't the case). Back in the day I used InstallShield for building our MSI packages, but now WiX is the best way to build .msi's -- by far. WiX helps a lot, but building appropriate MSI packages is still quite difficult. The following two pages helped me get to successfully building .msp's. Hopefully this concrete example will help others too.

ActiveFoo 1.0

For this example we'll build .msi installers for versions 1.0.0 and 1.0.1 of the the mythical "ActiveFoo" app ("activefoo-1.0.0.msi" and "activefoo-1.0.1.msi"). Then we'll build a '.msp' that will upgrade a 1.0.0 install to 1.0.1. We'll have the following files:

1.0.0/
    activefoo.wxs       # This describes "activefoo-1.0.0.msi"
    config.wxi
    installimage/       # The ActiveFoo install image
        CHANGES.txt
        foo.exe
        README.txt
1.0.1/
    activefoo.wxs       # This describes "activefoo-1.0.1.msi"
    config.wxi
    installimage/       # The install image with changes for 1.0.1
        CHANGES.txt
        README.txt
    upgrade-1.0.0.wxs   # This describes the '.msp'.
make.py                 # 'python make.py' to build everything
README.txt

Here is a zip of the working files for this example, if you'd like to play along.

We have a simple install image with three files (foo.exe, README.txt and CHANGES.txt). The WiX code to build an installer for ActiveFoo 1.0.0 is 1.0.0/activefoo.wxs:

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

<?include config.wxi ?>

<Wix xmlns="http://schemas.microsoft.com/wix/2003/01/wi">
  <Product Name="$(var.ProductName)" Id="$(var.ProductCode)"
           Language="1033" Codepage="1252" Version="$(var.ProductVersion)"
           Manufacturer="Acme" UpgradeCode="$(var.UpgradeCode)">

    <Package Id="????????-????-????-????-????????????" Keywords="Installer"
      Description="$(var.ProductName)"
      Comments="blah blah" Manufacturer="Acme"
      InstallerVersion="200" Languages="1033" Compressed="yes"
      SummaryCodepage="1252" />

    <Media Id="1" Cabinet="media.cab" EmbedCab="yes" />

    <!-- Define some of the dir-structure. -->
    <Directory Id="TARGETDIR" Name="SourceDir">
      <Directory Id="ProgramFilesFolder" Name="PFILES">
        <Directory Id="INSTALLDIR" Name="$(var.InstallId)"
                   LongName="$(var.InstallName)" />
      </Directory>
    </Directory>

    <!-- Define the feature hierarchy (just one feature in this simple
         example). -->
    <Property Id="INSTALLLEVEL" Value="1000" />
    <Feature Id="core" Title="ActiveFoo" Description="The Foo core"
             Level="1">
      <ComponentRef Id="MainExe" />
      <ComponentRef Id="ReadMeFiles" />
    </Feature>

    <!-- Define all the components. -->
    <DirectoryRef Id="INSTALLDIR">
      <Component Id="MainExe" Guid="6ee6fda3-6f50-47bf-99b9-6031c720428e">
        <File Id="MainExe" Name="foo.exe" DiskId="1"
              src="installimage\foo.exe" Vital="yes" />
      </Component>
      <Component Id="ReadMeFiles" DiskId="1"
                 Guid="8f2255f3-3eaf-4c82-9688-3545cd9b2018">
        <File Id="README.txt" Name="README.txt"
              src="installimage\README.txt" />
        <File Id="CHANGES.txt" Name="CHANGES.txt"
              src="installimage\CHANGES.txt" />
      </Component>
    </DirectoryRef>

  </Product>
</Wix>

with some configuration variables included from 1.0.0/config.wxi:

<?xml version="1.0" encoding="utf-8"?>
<Include>
  <?define ProductCode = "cdc5e50f-b490-4a37-8ff6-22e3cb3d690e" ?>
  <?define UpgradeCode = "ed340ed8-aa91-4bf6-9dcf-d7f6f4d43737" ?>

  <?define ProductName = "ActiveFoo" ?>
  <?define InstallName = "ActiveFoo" ?>
  <?define InstallId = "AFoo10" ?>
  <?define ProductVersion = "1.0.0" ?>
  <?define ProductURL = "http://www.example.com/products/activefoo/" ?>
</Include>

(Note that this WiX project is simplistic. In a real world WiX project you'd likely have a UI element for a user UI, define Add/Remove Programs -- ARP -- properties, etc.)

Use the provided "make.py" script to build "activefoo-1.0.0.msi":

C:\tmp\wix_and_msp> python make.py -v 100
INFO:make:build target '100'
DEBUG:make:running 'candle -nologo activefoo.wxs' in '1.0.0'
activefoo.wxs
DEBUG:make:running 'light -nologo -o ../activefoo-1.0.0.msi activefoo.wixobj' in '1.0.0'
INFO:make:'activefoo-1.0.0.msi' created

and install it. You should now have a "ActiveFoo" folder in your "Program Files".

ActiveFoo 1.0.1

Version 1.0.1 has the following changes:

  1. The ProductVersion is incremented to 1.0.1. We aren't change the ProductCode so this qualifies in MSI parlance as a "minor upgrade", as opposed to a "small update" or a "major upgrade").
  2. We've added a note to "CHANGES.txt" for the new release.
  3. We've removed the "foo.exe" file from the install image. This is so we can see how file removal can be accomplished with a "minor upgrade". There is a lot of documentation out there than says that file removal can't be done with an MSI minor upgrade. We'll see that that isn't true. I haven't seen any justification for why minor upgrades shouldn't remove files.

Normally, for these changes, the only updates to the WiX sources to build "activefoo-1.0.1.msi" would be to (a) update the "ProductVersion" string and (b) remove the File and Component elements for "foo.exe". However, working from this comment in Minor and Major Upgrades Using IPWI:

If you need to remove any files or registry data during the upgrade, add
records to the RemoveFile or RemoveRegistry tables of the newer database.

I've found that to get WiX to put a RemoveFile entry for, in this case, "foo.exe", I needed to add an explicit RemoveFile element:

      ...
      <Component Id="MainExe" Guid="6ee6fda3-6f50-47bf-99b9-6031c720428e">
        <!-- Note: This is how to explicitly remove files in an update. -->
        <RemoveFile Id="removefile1" On="install" Name="foo.exe"/>
      </Component>
      ...

The ProductVersion we updated in "1.0.1\config.wxi":

C:\tmp\wix_and_msp>diff -u 1.0.0\config.wxi 1.0.1\config.wxi
--- 1.0.0\config.wxi    Mon May 28 17:33:01 2007
+++ 1.0.1\config.wxi    Mon May 28 17:33:03 2007
@@ -6,7 +6,7 @@
   <?define ProductName = "ActiveFoo" ?>
   <?define InstallName = "ActiveFoo" ?>
   <?define InstallId = "AFoo10" ?>
-  <?define ProductVersion = "1.0.0" ?>
+  <?define ProductVersion = "1.0.1" ?>
   <?define ProductURL = "http://www.example.com/products/activefoo/" ?>
 </Include>

Now we can build "activefoo-1.0.1.msi":

C:\tmp\wix_and_msp> python make.py -v 101
INFO:make:build target '101'
DEBUG:make:running 'candle -nologo activefoo.wxs' in '1.0.1'
activefoo.wxs
DEBUG:make:running 'light -nologo -o ../activefoo-1.0.1.msi activefoo.wixobj' in '1.0.1'
INFO:make:'activefoo-1.0.1.msi' created

ActiveFoo 1.0.1 update

The basic process for building a '.msp' is:

  1. Get an administrative install of the old version. I hadn't known this before: An administrative install effective just extracts the file payload from an .msi into a given directory leaving a lighter .msi with just the MSI database tables. AFAIK this is the same thing as if you had built an "uncompressed MSI" -- i.e. one in which <Package Compressed='no' .../>. make.py will put this in "1.0.1\build\before".
  2. Get an administrative install of the new version. make.py will put this in "1.0.1\build\after".
  3. Write a WiX file that describes the patch.
  4. Compile to a Patch Creation Properties (.pcp) file with WiX.
  5. Compile to a '.msp' file with the "msimsp.exe" utility from the MSI SDK (Part of the Microsoft Platform SDK).

Here is a our WiX file describing the patch (1.0.1\upgrade-1.0.0.wxs) with comments inline:

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

<Wix xmlns='http://schemas.microsoft.com/wix/2003/01/wi'>
  <!-- TODO: Update PatchCreation Id for each new patch.
             Can we just use WiX's '????????-????-????-????-????????????' ? -->
  <PatchCreation Id='e8ee6400-7877-47e4-9519-ce17e3f1d59b'
                 CleanWorkingFolder='yes'
                 WholeFilesOnly='no'
                 AllowMajorVersionMismatches='yes'
                 AllowProductCodeMismatches='no'>

    <PatchInformation Description="ActiveFoo 1.0.1 Patch"
                      Comments='blah blah'
                      Manufacturer='Acme'
                      Languages='1033'
                      Compressed='yes' />

    <!-- TODO: Play with other values of 'Classification'. Does msiexec's
         behaviour actually change for different values? -->
    <PatchMetadata Description="ActiveFoo 1.0.1 Patch"
                   DisplayName="ActiveFoo 1.0.1 Patch"
                   TargetProductName='ActiveFoo 1.0'
                   ManufacturerName='Acme'
                   MoreInfoURL='http://www.example.com/products/activefoo'
                   Classification='Update'
                   AllowRemoval='yes' />

    <!-- From <http://wix.sourceforge.net/manual-wix2/patch_building.htm>
         """
         The SequenceStart value is influenced by the number of files that
         the previous patch delivered, as well as the number of files that
         this patch will deliver. This tells PatchWiz.dll to start assigning
         File sequence numbers from this number. So if this patch ships 11
         files, and the next patch uses a SequenceStart of 1020, it will step
         on the 11th file's assigned sequence number. In this case the next
         patch would use a SequenceStart of 1030, and 03 as the patch id to
         avoid conflicts with this patch. This scheme helps prevent this by
         coordinating the SequenceStart (file sequence numbers) with the
         patch sequence number. Also, note that the SequenceStart of the
         first patch must be greater than the number of files in the original
         installation. If the original installation contained more than 1000
         files(rare), then the SequenceStart for the first patch must be set
         to a higher value (e.g 2010.)
         """
    -->
    <!-- Name is max 8 chars. *How* unique does this have to be? -->
    <Family Name='Fam101' DiskId='2' MediaSrcProp='AFoo10_2_1_01'
            SequenceStart='1010'>
      <UpgradeImage Id='AFoo10Upgrade'
                    SourceFile='after\activefoo-1.0.1.msi'>
        <TargetImage Id='AFoo10Target' Order='1' IgnoreMissingFiles='no'
                     SourceFile='before\activefoo-1.0.0.msi' />
      </UpgradeImage>
    </Family>

    <TargetProductCode Id='cdc5e50f-b490-4a37-8ff6-22e3cb3d690e' />
  </PatchCreation>
</Wix>

Use make.py to build the patch:

C:\tmp\wix_and_msp> python make.py -v 101_upgrade
INFO:make:build target '101_upgrade'
DEBUG:make:running 'msiexec /a activefoo-1.0.0.msi TARGETDIR=C:\tmp\wix_and_msp\1.0.1\build\before'
DEBUG:make:running 'msiexec /a activefoo-1.0.1.msi TARGETDIR=C:\tmp\wix_and_msp\1.0.1\build\after'
        1 file(s) copied.
DEBUG:make:running 'candle -nologo upgrade.wxs' in 'C:\tmp\wix_and_msp\1.0.1\build'
upgrade.wxs
DEBUG:make:running 'light -nologo upgrade.wixobj' in 'C:\tmp\wix_and_msp\1.0.1\build'
DEBUG:make:running '"C:\Program Files\Microsoft Platform SDK\Samples\SysMgmt\Msi\Patching\MsiMsp.Exe" -s upgrade.pcp -p C:\tmp\wix_and_msp\activefoo-1.0.1-upgrade-1.0.0.msp -l upgrade.log' in 'C:\tmp\wix_and_msp\1.0.1\build'
INFO:make:'activefoo-1.0.1-upgrade-1.0.0.msp' created
INFO:make:To install the update, run:
  msiexec /p activefoo-1.0.1-upgrade-1.0.0.msp REINSTALL=ALL REINSTALLMODE=omus

You should now be able to install "activefoo-1.0.1-upgrade-1.0.0.msp" over an ActiveFoo 1.0.0 installation to upgrade to ActiveFoo 1.0.1. Note that some docs out there mention an MSI bug preventing installation of a '.msp' by double-clicking on that. I've found that I am able to install by double-clicking on my WinXP box with Windows Installer V 3.01.4000.1823.

Notes/Limitations

  1. Having to explicitly put in RemoveFile elements to ensure upgrades remove them is a pain. It would be nice if WiX inferred that automatically. WiX v3 is slated to include "Patch creation support" and "ClickThrough". Perhaps these will go a long way to making all of this easier.
  2. There are many variables to tweak here that I haven't played with. I haven't deployed any .msp's built as describe here to users on any scale so I there may be gremlins lurking in this procedure.

I'd be happy to hear about others' experiences working with WiX and MSI patches.

Tagged: wix, install, komodo, programming, mozilla