WiX for dummies

We are now successfully using WiX to make MSIs as part of our automated build process. Currently the MSIs are nothing fancy and have no custom actions. They look and act exactly like the MSIs that Visual Studio .NET produce. Following are the steps I followed. I'm sure there are better ways to accomplish this, but it's working for now, and maybe putting this recipe out there will encourage someone else to try playing with WiX, or to share how they are using it.

One goal was to have a very low-tech method of including new files into our installers. I didn't want our developers to have to know anything about WiX to add new files or directories to the distribution, nor to have to change the WiX scripts at all. To accomplish that, we use NAnt to just copy everything to a distributable image exactly as we want it to be on an end-user's machine. A script (js) then recursively walks through this image and generates the appropriate WiX script to install the files. In this way, all a developer has to do is make sure the file or directory is copied to the proper place in the NAnt script before the WiX script is generated.

I'm interested in hearing how others are getting along with WiX.

Step 1) I created a bare-bones installer using Visual Studio .NET 2003. It didn't even install anything, I just needed it to reverse engineer it to get all of the UI.

  • Launch Visual Studio .NET 2003
  • Select File | New Project
  • Select Setup and Deployment Projects and create a new Setup Project (ex: Setup1.vdproj)
  • Then just do a Build

Step 2) I used WiX's dark.exe to reverse engineer the MSI produced in step 1 into a .wxs file.

  • dark.exe Setup1.msi Setup1.wxs

NOTE: Several people have pointed out that running dark.exe with the /x switch will eliminate the need for step 3. Thanks!

Step 3) I used the Orca tool to extract the three binary image files from the MSI.

  • Open Setup1.msi in Orca
  • select the Binary table
  • double click each of the three [Binary Data] cells on the right and choose Write binary to filename (I exported them to a subdirectory under the wxs name Binary because that's where the wxs will look for them). Export these three:
    • Binary\DefBannerBitmap.ibd
    • Binary\NewFldrBtn.ibd
    • Binary\UpFldrBtn.ibd

Now I threw away the Visual Studio project and the MSI for good!

Step 4) I modified the wxs by replacing the <Directory> and <Feature> elements with an include that I'll explain in a bit. The <Media> element was also modified.

Here is what it looked like before the modifications:

<Wix ...>
  <Product ...>
    <Package ... >
    <Directory Id="TARGETDIR" Name="SourceDir">
      <Component Id="C_DefaultComponent" Guid="4C231858-2B39-11D3-8E0D-00C04F6837D0" KeyPath="yes">
        <Condition>0</Condition>
      </Component>
      <Directory Id="ProgramMenuFolder" Name="." SourceName="USER'S~1" LongSource="User's Programs Menu" />
      <Directory Id="DesktopFolder" Name="." SourceName="USER'S~2" LongSource="User's Desktop" />
    </Directory>
    <Feature Id="DefaultFeature" Level="1" ConfigurableDirectory="TARGETDIR">
      <ComponentRef Id="C_DefaultComponent" />
    </Feature>
    <Media Id="1" />
    <CustomAction...>

I changed it to look like this:

<Wix ...>
  <Product ...>
    <Package ... >

    <?include Setup1.wxi ?>   
   
    <Media Id="1"
           Cabinet="Setup1.cab"
           EmbedCab="yes" />

    <CustomAction...>

Step 5) At the end of our automated build process, we assemble a distributable image exactly as we want things to be installed on an end-user machine. (We just copy all the files in the correct places using NAnt)

Step 6) NAnt then invokes a javascript (the script is included below) that recursively walks through the distributable image and generates an include (wxi) file that looks something like this:

---Setup1.wxi--- (generated)

<?xml version="1.0" encoding="UTF-8" ?>
<Include>
  <Directory Id="TARGETDIR" Name="SourceDir">
    <Component Id="C__7E70E5E5CE194270A740223A09796433" Guid="7E70E5E5-CE19-4270-A740-223A09796433">
      <FileGroup filter="*.*" Prefix="7E70E5E5CE194270A740223A09796433" src="$(var.redist_folder)" DiskId="1"/>
    </Component>
    <Directory Id="_BEDE2FEE99504DFCACD32A59F864D525" Name="ABC" LongName="abc">
      <Component Id="C__2BAC5C9497D9446BBA06AC5445338286" Guid="2BAC5C94-97D9-446B-BA06-AC5445338286">
        <FileGroup filter="*.*" Prefix="2BAC5C9497D9446BBA06AC5445338286" src="$(var.redist_folder)\abc" DiskId="1"/>
      </Component>
    </Directory>
  </Directory>
  <Feature Id="DefaultFeature" Level="1" ConfigurableDirectory="TARGETDIR">
    <ComponentRef Id="C__7E70E5E5CE194270A740223A09796433" />
    <ComponentRef Id="C__2BAC5C9497D9446BBA06AC5445338286" />
  </Feature>
</Include>

This include file is what is included in the wxs modified in step 4

Step 7) NAnt then invokes candle.exe and light.exe to compile it into an MSI.

The whole NAnt process (simplified) looks something like this:

<project name="Installer" default="installer">

    <target name="installer">

        <!-- Assemble the redistributable directory structure
             by pulling files from various locations. -->

        <mkdir dir="redist"/>
        <copy file="Readme.txt" todir="redist"/>
        <!-- etc... -->

        <!-- Generate the include -->
        <exec program="cscript.exe">
            <arg value="generate-install-script.js" />
            <arg value="${nant.project.basedir}\redist" />
            <arg value="Setup1.wxi" />
        </exec>

        <!-- compile the wxs, note how variables are passed into wix -->
        <exec program="C:\tools\WiX\candle.exe">
            <arg value="Setup1.wxs" />
            <arg value="-dredist_folder=redist" />
            <arg value="-dversion=1.0.0.1" />
        </exec>

        <!-- link it -->
        <exec program="C:\tools\WiX\light.exe"
              commandline="Setup1.wixobj -out Setup1.msi" />
    </target>
</project>

--- generate-install-script.js ---

The script that generates the include looks like this:

// Generates the WiX XML necessary to install a directory tree.
var g_shell = new ActiveXObject("WScript.Shell");
var g_fs = new ActiveXObject("Scripting.FileSystemObject");
if (WScript.Arguments.length != 2)
{
    WScript.Echo("Usage: cscript.exe generate-install-script.js <rootFolder> <outputXMLFile>");
    WScript.Quit(1);
}
var rootDir = WScript.Arguments.Item(0);
var outFile = WScript.Arguments.Item(1);
var baseFolder = g_fs.GetFolder(rootDir);
var componentIds = new Array();

WScript.Echo("Generating " + outFile + "...");

var f = g_fs.CreateTextFile(outFile, true);
f.WriteLine("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
f.WriteLine("<Include>");
f.WriteLine("  <Directory Id=\"TARGETDIR\" Name=\"SourceDir\">");
f.Write(getDirTree(rootDir, "", 1, baseFolder, componentIds));
f.WriteLine("  </Directory>");
f.WriteLine("  <Feature Id=\"DefaultFeature\" Level=\"1\" ConfigurableDirectory=\"TARGETDIR\">");
for (var i=0; i<componentIds.length; i++)
{
    f.WriteLine("    <ComponentRef Id=\"C__" + componentIds[i] + "\" />");
}
f.WriteLine("  </Feature>");
f.WriteLine("</Include>");
f.Close();

// recursive method to extract information for a folder
function getDirTree(root, xml, indent, baseFolder, componentIds)
{
    var fdrFolder = null;
    try
    {
        fdrFolder = g_fs.GetFolder(root);
    }
    catch (e)
    {
        return;
    }

    // indent the xml
    var space = "";
    for (var i=0; i<indent; i++)
        space = space + "  ";

    if (fdrFolder != baseFolder)
    {
        var directoryId = "_" + FlatFormat(GetGuid());

        xml = xml + space + "<Directory Id=\"" + directoryId +"\"";
        xml = xml + " Name=\"" + fdrFolder.ShortName.toUpperCase() + "\"";
        xml = xml + " LongName=\"" + fdrFolder.Name + "\">\r\n";
    }

    var componentGuid = GetGuid();
    var componentId = FlatFormat(componentGuid);

    xml = xml + space + "  <Component Id=\"C__" + componentId + "\""
              + " Guid=\"" + componentGuid + "\">\r\n";
    xml = xml + space + "    <FileGroup filter=\"*.*\" Prefix=\""
              + componentId + "\" src=\"$(var.redist_folder)"
              + fdrFolder.Path.substring(baseFolder.Path.length)
              + "\" DiskId=\"1\"/>\r\n";
    xml = xml + space + "  </Component>\r\n";

    componentIds[componentIds.length] = componentId;

    var enumSubFolders = new Enumerator(fdrFolder.SubFolders);

    var depth = indent + 1;
    for (;!enumSubFolders.atEnd();enumSubFolders.moveNext())
    {
        var subfolder = enumSubFolders.item();
        xml = getDirTree(enumSubFolders.item().Path, xml, depth, baseFolder, componentIds);
    }

    if (fdrFolder != baseFolder)
    {
        xml = xml + space + "</Directory>\r\n";
    }

    return xml;
}

// Generate a new GUID by calling uuidgen
function GetGuid()
{
    var sysEnv = g_shell.Environment("SYSTEM");
    var oExec = g_shell.Exec(sysEnv("VS71COMNTOOLS") + "uuidgen.exe");
    var input = "";

    while (!oExec.StdOut.AtEndOfStream)
    {
        input += oExec.StdOut.Read(1);
    }
    return input.substring(0,36).toUpperCase();
}

// Convert a GUID from this format
//   7e70e5e5-ce19-4270-a740-223a09796433
// to this format:
//   7E70E5E5CE194270A740223A09796433
function FlatFormat(guid)
{
    var re = /-/g;
    return guid.toUpperCase().replace(re, "");
}

12 Comments

  • The decompiler, dark.exe is broken in 2.0.1706.0 and generates invalid wxs. The steps above work with the previous version (2.0.1629.0).

  • We have been looking for a good way to replace our .zip file packages with msi's. This is great! I will post back on how this works in our environment.

  • Why not &quot;dark.exe /x&quot; instead of step 3?

  • Great post!



    Here's a slightly shorter way of making a guid in JScript:



    function GetGuid() {

    return new ActiveXObject(&quot;Scriptlet.Typelib&quot;).Guid.substr(0,38);

    }

  • Awesome read. Thanks!

  • Apparently the FileGroup element is not the best choice as it breaks Component rules according to Rob Mensching in his blog

  • Help me out here. I totally get &lt;arg value=&quot;-dversion=1.0.0.1&quot; /&gt;; it passes the version into candle, which is then used in your &lt;product&gt; element... but what is the syntax for referencing a -d variable in a wxs file? I tried ${sys.version}, I tried ${env.version}, ${var.version}. nothing seemed to work. What's up with that?

  • Never mind me - I had ${} where I needed $(). Thanks anyhow.

  • Indepently of your solution, I have written a custom MSBuild task that generates the same typ of output in WIX3 format.

    Great minds think alike!

    Pete

  • Pete, would you be willing to share your MSbuild task. I am just about to write one but thought, "Why re-invent the wheel"

  • I have had great success with WIX generating MSI files for VB6 projects from NAnt. I would like to add .reg files now to be included in the MSI and added to the registry, I have three versions which one is chosen based on a config in NAnt for Production, Development and UAT.

    I have seen how to hard code the registry values into WIX but is there a method to simply include the .reg file and exec that on the client as part of the install?

    Many thanks in advance.

  • This doesn't look like a Wix for Dummies article at all. First of all, a "Dummies" article shouldn't include using nAnt, and I would expect alot of "handholding" too. I found this article because I'm still trying to wrap my head around the concept of what is a Keypath... I didn't see anything here on that other than the actual keyword.

Comments have been disabled for this content.