My previous post details a method to use WiX and NAnt to automatically generate MSI's as part of an automated build. I'm going to recommend that no one follow the part of that example that auto-generates the WiX XML. I'm going to blame my spam filter. Let me explain...
A while ago I subscribed to the WiX mailing list. Unfortunately my spam filter was routing all posts to my junk folder so I wasn't reading them. If I had been reading them I would have noticed a lively debate on the use of the <FileGroup> element and how it is generally considered to be evil-incarnate. So much so that Rob Mensching is talking about removing it from WiX altogether so no one is tempted to misuse it.
FileGroup makes it far too easy to violate Windows Installer Component Rules. The SourceForge WiX mailing lists are not archived yet, but there will be detailed explanations of why its dangerous in posts found in the wix-users list soon.
Once you have defined a component (a set of files), the Component ID (which is a GUID) must not change unless the contents of the component changes. The technique I laid out generated new Component IDs every single build.
The current direction members of the WiX list seem to be leaning involves having a tool to initially generate the WiX XML fragments, checking them in to your source control, and then hand-maintaining them as changes are needed. Although auto-generating each time is handy from a developer perspective because new files are magically picked up, it is a nightmare when it comes to installing upgrades.
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, "");
}