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>