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, "");
}