Preventing Same Version Downgrades with WiX

14Mar11

So I farked up when I was building several MSI packages using the Microsoft Windows XML (WiX) Toolset. I broke a sacred covenant of MSI development and violated the component rules.

This would not normally be a huge concern since the members of the components, the files, are not going to change. However, I did run into an interesting issue. Because my company’s versioning scheme is MAJOR.MINOR.REVISION.BUILD where BUILD is a value auto-incremented by the build server, as far as MSI is concerned 4.0.0.123 is the same version as 4.0.0.187. And because our patch strategy is to simply release a hot-fixed MSI, we have to allow same-version upgrades.

Anyone familiar with MSI upgrades knows the dirty secret that allowing same version upgrades allows same version downgrades. Except these downgrades would result in files disappearing from the filesystem. After much research I determined the issue was because of my stupid decision to group multiple files into single Components. MSI was not applying the standard file versioning rules correctly, and thus during a downgrade the following steps were occurring:

  1. Version X is installed on a system and user attempts to install version X-1.
  2. The installer appears to calculate which Components need to be installed by version and date. It does not include the X-1’s Components because they are already on the system.
  3. The installer removes X’s Components from the file system.
  4. Finally, the installer chooses not to install X-1’s Components because the installer has already calculated that the Components on the filesystem, at version X, were newer.

I found myself in quite a pickle. How do I prevent same version downgrades when the only varying component of the version is the 4th component (which again, MSI ignores)?

First of all, let me clear the air and say that I’m using the original WiX method of handling major upgrades.

<Upgrade Id="$(var.UpgradeGuid)">
  <UpgradeVersion Minimum="3.9.9" IncludeMinimum="yes" Maximum="$(var.Version)" IncludeMaximum="yes" Property="OLDERVERSIONBEINGUPGRADED" />
  <UpgradeVersion Minimum="$(var.Version)" IncludeMinimum="no" OnlyDetect="yes" Property="NEWERVERSIONDETECTED" />
</Upgrade>
<CustomActionRef Id="WixExitEarlyWithSuccess"/>

This bit of code uses the WiX’s custom action WixExitEarlyWithSuccess in conjunction with the NEWERVERSIONDETECTED property to detect whether or not a new version is detected, and thus the installer will exist early with a successful error code (ERROR_NO_MORE_ITEMS) if a newer version of the product is already installed. I know that more recent versions of WiX include a MajorUpgrade element that makes this process more concise (cleaner), but this new method does not work in conjunction with the WixExitEarlyWithSuccess custom action.

Since the WixExitEarlyWithSuccess custom action works by reading the NEWERVERSIONDETECTED property, I thought I’d be tricky and try populating that property myself. To that end I used a FileSearch element to look for a key file that I knew would always have the same MAJOR.MINOR.REVISION.BUILD version that I needed to compare against.

<Property Id="NEWERVERSIONDETECTED">
  <DirectorySearch Id="NewerFileVersionDirSearch" Path="[INSTALLDIR]">
    <FileSearch Name="KeyFile.dll" MinVersion="$(var.Version4)"/>
  </DirectorySearch>
</Property>

This worked perfectly! Or so I thought. While this fix worked in conjunction with WiX’s own WixExitEarlyWithSuccess custom action during UI-enabled installations, it failed when the MSI was run in any type of quiet mode. A quick examination of the MSI log files with the verbose option enabled revealed that it was because the WixExitEarlyWithSuccess custom action is scheduled after FindRelatedProducts and my instance of the NEWERVERSIONDETECTED property is not set yet when run in silent mode when the WixExitEarlyWithSuccess custom action is processed.

My solution was to create my own custom action: ExitEarlyWithSuccess.

<CustomAction Id="ExitEarlyWithSuccess" VBScriptCall="Main" Property="ExitEarlyWithSuccessScript" />

<InstallExecuteSequence>
  <Custom Action="ExitEarlyWithSuccess" After="InstallInitialize">NOT Installed AND NEWERFILEVERSIONDETECTED</Custom>
</InstallExecuteSequence>

<Property Id="ExitEarlyWithSuccessScript">
  <![CDATA[
    Function Main()
      Main = 5
    End Function
  ]]>
</Property>

<Property Id="NEWERFILEVERSIONDETECTED">
  <DirectorySearch Id="NewerFileVersionDirSearch" Path="[INSTALLDIR]">
    <FileSearch Name="KeyFile.dll" MinVersion="$(var.Version4)"/>
  </DirectorySearch>
</Property>

The secret to my custom action is that it is scheduled after InstallInitialize giving the normal WiX properties, such as Installed a chance to be populated. The ExitEarlyWithSuccessScript is an embedded VBScript that returns the ERROR_NO_MORE_ITEMS code to indicate to the installer that it should exit immediately with a successful exit code. Also, the custom action is set to run only when the product is not installed since you don’t want this action to occur during maintenance or removal (not that it matters too much since the file version comparison wouldn’t match — the MinVersion attribute in a File element matches the next version that it is compared to. For example, 4.0.0.0 would match 4.0.0.1, but not 4.0.0.0. In this way, MinVersion will never match the currently installed version).

I’ve tested this method of exiting an installer early over several MSI files now, and it works for each one of them.

Hope this helps!

About these ads


One Response to “Preventing Same Version Downgrades with WiX”

  1. Wll man, this is from a while ago, but VBScripts are widely un-recommended since they have a lot of pitfalls (for example, your antivirus may block them and you’re out!). Creative solution anyway ;)


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


Follow

Get every new post delivered to your Inbox.

%d bloggers like this: