WiX For Drunks
The WiX Toolset, the definitive command line toolset for creating Microsoft Installer packages, is famous for being a royal pain in the ass to learn. This tutorial attempts to cut through the problems this author encountered with the existing free reading material. Mostly it will introduce from the beginning the concepts you will need to understand to avoid encountering bajillions of confusing errors the very instant you exceed the use cases available in a tutorial.
For some baffling (probably historical) reasons WiX is broken down into a compiler and linker called
candle and light. For some equally baffling reason they aren't added to your path, but their
installation directory is written to %wix%.
Here is an example of the DOS commands I use in a current project. candle writes to
build\ARCH.wixobj and light takes over from there to produce an .msi in build\.
"%wix%bin\candle.exe" winstaller.wxs -ext WixUtilExtension -arch x86 -dPlatform=x86 -out build\x86.wixobj
"%wix%bin\candle.exe" winstaller.wxs -ext WixUtilExtension -arch x64 -dPlatform=x64 -out build\x64.wixobj
"%wix%bin\light.exe" -ext WixUIExtension -ext WixUtilExtension build\x64.wixobj -out build\PornViewer_x64.msi -sw1076
"%wix%bin\light.exe" -ext WixUIExtension -ext WixUtilExtension build\x86.wixobj -out build\PornViewer_x86.msi -sw1076
The -sw1076 switch suppresses this
irritating warning. When ICE69 is thrown at warning level, it indicates that one Component
references another but they're properly dependent and that's fine. If you reference something in a
Component that might not be installed you will get ICE69 at error level. You can't suppress
errors so don't try. It is recommended to use this switch all the time.
Two extensions are being used. You will most likely want to use WixUIExtension as it's necessary
if you want a GUI for your installer. I recently started using the WixUtilExtension because it
provides RemoveFolderEx which is the easiest way to clean up a directory full of junk left outside
the primary install directory. We'll demonstrate it's use here to show the gotchas of using
extensions.
First, go here and generate a fistfull of guids. When Windows Installer checks its records for the presence of your feature (or entire package) it searches by guid.
This example is a per-machine
install that places its executables in the traditional program files directory. It packs all its
files inside the .msi and uses the common WixUI_FeatureTree gui with a EULA page. Because it
uses the WixUtilExtension extension it must define the util namespace. Curiously, the
WixUIExtension namespace is built in.
<?xml version="1.0" encoding="UTF-8"?>
<Wix
xmlns="http://schemas.microsoft.com/wix/2006/wi"
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension"
>
<Product
Id="4d5791d8-d5f5-46b7-bf16-2221771712f9"
Name="PornViewer"
Language="1033"
Version="0.0.1"
Manufacturer="Schmidty's Superior Solutions"
UpgradeCode="8527e354-e187-4921-8af5-de92e1004b4a"
>
<Package
InstallerVersion="200"
InstallScope="perMachine"
Compressed="yes"
Comments="Windows
Installer Package"
/>
<Media
Id="1"
Cabinet="product.cab"
EmbedCab="yes"
/>
<UIRef Id="WixUI_FeatureTree" />
<WixVariable Id="WixUILicenseRtf" Value="LICENSE.rtf" />To see all the available options on the root elements, you are advised to peruse the schema documentation. Don't worry about Bundles, Modules or Patches yet.
Your installer is built of a flat list of Components. The world sees your installer as a simple
heirarchy of Features. Each Feature may contain any number of ComponentRef elements which map
each Feature to the Component elements on which it depends. You can and probably should
reference the same Component multiple times. Together this adds up to a fairly powerful dependency
management system.
To put it simply, a Component is a software resource and a Feature is how the end-user sees your
application.
In this example, the start menu shortcut and file type associations are specified as optional child
Feature elements. Note the way that ApplicationFiles is required multiple times. This shouldn't
be necessary because the enclosing feature already references it. WiX could resolve references
across these Components as safe. However, it does not do that. You'll get ICE69 in both cases
but without the duplicate refs you get it at error level. At minimum that's bad because you might
not notice when you get a for-real ICE69 error that needs to be addressed. Your .msi will also
not validate.
<Feature Id="Complete" Title="PornViewer 0.0.1" Description="The complete package." Display="expand" Level="1" ConfigurableDirectory="INSTALLDIR">
<ComponentRef Id="ApplicationFiles" />
<ComponentRef Id="AppDataDirectory" />
<Feature
Id="StartMenuFeature"
Level="1"
Title="Start Menu shortcuts"
Description="Add PornViewer to the Start Menu"
AllowAdvertise="no"
InstallDefault="local"
>
<ComponentRef Id="ApplicationShortcuts" />
</Feature>
<Feature
Id="FiletypesFeature"
Level="100"
Title="File Associations"
Description="Open image files with PornViewer"
Display="expand"
AllowAdvertise="no"
InstallDefault="local"
>
<Feature
Id="JpgTypesFeature"
Level="100"
Title="*.jpg and *.jpeg"
Description="Open JPEG images with PornViewer"
AllowAdvertise="no"
InstallDefault="local"
>
<ComponentRef Id="ApplicationFiles" />
<ComponentRef Id="JpgAssociation" />
</Feature>
<Feature
Id="GifTypesFeature"
Level="100"
Title="*.gif"
Description="Open GIF images with PornViewer"
AllowAdvertise="no"
InstallDefault="local"
>
<ComponentRef Id="ApplicationFiles" />
<ComponentRef Id="GifAssociation" />
</Feature>
<Feature
Id="PngTypesFeature"
Level="100"
Title="*.png"
Description="Open PNG images with PornViewer"
AllowAdvertise="no"
InstallDefault="local"
>
<ComponentRef Id="ApplicationFiles" />
<ComponentRef Id="PngAssociation" />
</Feature>
</Feature>
</Feature>Before you can declare a Component that contains files, you must declare some Directory
elements. The way WiX does this is a hack burrito with hack sauce. A Directory with its Id
property set to certain magic values
will be positioned automatically by Microsoft Installer during execution.
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="$(var.PlatformProgramFilesFolder)">
<Directory Id="INSTALLDIR" Name="PornViewer">
<Component
Id="ApplicationFiles"
Guid="8527e354-e187-4921-8af5-de92e1004b4a"
>Every Component must have a keypath. It's some sort of backup check to see whether your Component is
installed already. A per-machine Component such as the traditional pattern of installing the primary
executable(s) to Program Files may use a file or folder as a keypath. When the keypath is not
specified the enclosing Directory will be used. You may manually specify that a File,
Directory, or RegistryValue element is your keypath with KeyPath="yes".
<Component Id="ApplicationFiles" Guid="8527e354-e187-4921-8af5-de92e1004b4a">
<CreateFolder />
<File Id="Executable" Source="build\$(var.Platform)\nw.exe" Vital="yes" KeyPath="yes" />Per-user Components must have a per-user keypath. For reasons this is not satisfied by the existence
of a file or directory inside %appdata%. You explicitly must use a registry key under
HKEY_CURRENT_USER. A quick note: in WiX you set which registry root you're using with an acronym
of it's name. Root="HKCU" or HKLM or HKCR etc.
So let's look at an example. First we write a shortcut in the user's start menu. We also override
the name of the Product's primary executable by setting the "FriendlyAppName" in the registry.
This is because PornViewer is a node-webkit application and its executable has to be called
nw.exe. So that you aren't playing Where's Waldo, the first RegistryValue is set to be the
keypath by its last property.
<Directory Id="ProgramMenuFolder">
<Directory Id="ProgramMenuSubfolder" Name="PornViewer">
<Component Id="ApplicationShortcuts" Guid="0093168f-dbc1-430e-b63f-570a01356f3c">
<Shortcut
Id="ApplicationShortcut1"
Name="PornViewer 0.0.1"
Description="View Some Porn"
Target="[INSTALLDIR]nw.exe"
WorkingDirectory="INSTALLDIR"
Icon="ProductIcon"
/>
<RegistryValue
Root="HKCU"
Key="Software\Microsoft\Windows\ShellNoRoam\MUICache"
Name="[$(var.PlatformProgramFilesFolder)]PornViewer\nw.exe.FriendlyAppName"
Value="PornViewer"
Type="string"
KeyPath="yes"
/>
<RegistryValue
Root="HKCU"
Key="Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\MuiCache"
Name="[$(var.PlatformProgramFilesFolder)]PornViewer\nw.exe.FriendlyAppName"
Value="PornViewer"
Type="string"
/>
<RemoveFolder Id="ProgramMenuSubfolder" On="uninstall" />
</Component>
</Directory>
</Directory>