building MSI patch packages (.msp) with WiX
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:
- 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").
- We've added a note to "CHANGES.txt" for the new release.
- 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:
- 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". - Get an administrative install of the new version. make.py will put this in "1.0.1\build\after".
- Write a WiX file that describes the patch.
- Compile to a Patch Creation Properties (.pcp) file with WiX.
- 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
- 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.
- 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.