WiX and cabinetry

WiX v3.6 is focused on bring the Burn chainer into everyone’s hands. (You might be surprised how many products are already built with it.) That said, other features have been introduced in v3.6 and some of them are surprisingly useful. This post is about two of the more useful features, both dealing with cabinets.

Media templates

There are advantages to having multiple cabinets for your MSI packages, from build-time performance to more reliable downloading. Unfortunately, up to WiX v3.5, WiX required you to explicitly identify a set of files to go into a second (or third or fourth or…) cabinet using the DiskId attribute on the Component, File, Directory, or DirectoryRef elements.

WiX v3.6’s MediaTemplate element automatically creates cabinets up to an optionally-specified size. Files are assigned to “fill” each cabinet.

Note: MediaTemplate replaces the Media element; having both in your authoring results in a link-time error. You can still manually author DiskId attributes but they’re ignored in favor of MediaTemplate’s automatic cabinet making.

The simplest use of MediaTemplate is an empty element that takes all the defaults:

<MediaTemplate />

MediaTemplate will generate as many cabinets as necessary for all the files in your package, up to a maximum of 999 cabinets. (If you have more than that, good luck!) Each cabinet will:

  • Be named Prod###.cab
  • Be external to the package (not embedded)
  • Store up to 200MB of files before compression

MediaTemplate shares some of Media’s attributes, to cover the same functionality:

  • CompressionLevel specifies the compression level to use in each cabinet.
  • DiskPrompt specifies the disk name and VolumeLabel the volume label, if you’re dealing with multi-disk installs.
  • EmbedCab lets you embed the cabinets in the .msi package. The default is to keep the cabinets external.

Attributes specific to MediaTemplate control how the cabinets are generated and filled:

  • CabinetTemplate names each cabinet. The default is “Prod{0}.cab” which yields cabinet names of Prod1.cab, Prod2.cab, and so forth. The “{0}” is replaced with cabinet numbers, from 1 to 999. MSI says that cabinets have to follow the rules for 8.3 short filenames, so you can have up to five whole characters to distinguish your cabinet names.
  • MaximumUncompressedMediaSize, as its impressively-long name indicates, specifies the maximum size of the files in each cabinet, in megabytes. The default size is 200MB.MediaTemplate counts actual file sizes—without compression—as it adds files to cabinets. Doing so is more predictable than trying to add files to cabinets as part of the cabinet-creation process; WiX can’t accurately predict how much a particular file will take up once it’s compressed so adding a file to a cabinet might make it larger than the maximum size. Using the uncompressed file size means that compressed cabinets will generally be smaller than the maximum size. On the flip side, any individual files that are larger than the maximum size go into a single-file cabinet, so it’s possible that such cabinets will be larger than the maximum size.

You can override MaximumUncompressedMediaSize by specifying the size in megabytes in the WIX_MUMS environment variable. You wouldn’t typically do so but using an environment variable makes it easy to tweak the cabinet size on a developer box, say to create more, smaller cabinets to take advantage of WiX’s multithreaded cabinet creation to speed up builds on a developer box with many cores.

Overriding compression level

Speaking of speeding up developer builds…Back in 2008, I added the default compression level feature to let you specify compression level at the project level instead of just in WiX authoring. That feature let you change compression level without changing authoring but you still had to change your .wixproj or other build script.

Now in WiX v3.6, you can override the compression level by specifying the WIX_COMPRESSION_LEVEL environment variable with the same values you can specify in, for example, the MediaTemplate/@CompressionLevel attribute. WIX_COMPRESSION_LEVEL overrides the default compression level specified in .wixproj MSBuild projects and explicitly authored @CompressionLevel attribute values.

There’s one simple use case: WIX_COMPRESSION_LEVEL lets developers specify “none” or “low” compression to avoid pegging all their CPU cores doing “mszip” or “high” compression that they absolutely don’t care about.

Tactical directory nukes

Though I’ve commented before about unnecessarily “nuking” whole registry keys, sometimes it’s what’s needed. The same applies to the file system: Sometimes apps create files and directories that weren’t part of the original installation. MSI makes it easy to remove files, including via wildcard. Individual directories can also be removed, but MSI doesn’t have support for wildcard directories or recursive directory trees. It’s easy to delete trees via custom action but tougher to do so while also handling rollback.

Thanks to Rob’s work on wixcontrib, the code to handling directory nuking was available. I took it with some improvements into WixUtilExtension in WiX v3.6 and it’s available with today’s build: 3.6.1321.0.

Use the RemoveFolderEx element as a child of a Component:

<Component …>
<util:RemoveFolderEx On=”uninstall” Property=”MyDirtyDirectory” />
</Component>

There is one annoying restriction: RemoveFolderEx cannot be used directly on a directory defined in your installer. It’s an annoying chicken/egg problem. As adding directories and files to be deleted affects MSI’s file costing, RemoveFolderEx has to do its thing before costing. Unfortunately, MSI doesn’t set up properties for target directories until after costing is complete.

According to a poll both unscientific and informal, the most common use case is that the directory path is already written to a registry value anyway, for use by the installed application. See Rob’s blog post The WiX toolset’s “Remember Property” pattern for an example. As the AppSearch standard action sets the property well before costing begins, RemoveFolderEx works.

Quick implementation note: RemoveFolderEx works by recursing from the specified directory, adding temporary rows to the RemoveFile table for each subdirectory, one for the directory and one for the files in each directory. This is the pattern I described as “semi-custom actions” way back in 2007.