Wix

In finally get CruiseControl.Net set up to not only automate our build process, but notify us of any breaking changes on checkin, I discovered some issues in using the standard setup and deployment projects. Not wanting to spend a lot of time on it, I decided to look into Wix and see how it could help me streamline things.

It turns out Wix is a whole lot more work. Instead of a simple point and click interface, I’m presented with hand editing the XML file. Boo.

There are a few decent open source projects to give me a GUI interface so I can get my point and click back, but they turned out to either be for Wix 2 or they had some serious bugs in them.

In the end I used Dark to script my MSI file. While this worked out ok, Wix 3 wouldn’t compile the script without me making some changes. I dug through the script it created and I started to wonder how I was going to automate this. The trick is really not to automate it. The trick is to set up your script once and then leave it alone. Modifications are only made when things change in your build. That’s fine, but what about setting it up for the first time? You have to create a GUID for each component (read, every file if you want the files updateable) that gets installed. This can be time consuming if you have quite a few ancilary files that need to be shipped with the release.

Well, I automated that part. I created myself a small console application that will take a folder and a output file name as arguments. The program will traverse this folder and create an include script. It treats every file, directory, and directory of files as individual components.

In case anyone wants to do this but doesn’t want to take the time to write all the code, here it is.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml;
using System.Xml.XPath;
using System.Reflection;

namespace CreateIncludeScript
{
    class Program
    {
        static int Main(string[] args)
        {
            if (args.Length < 2 || args.Length > 2)
            {
                return (-1);
            }
            string folder = args[0];
            string outputfile = args[1];
            XmlDocument xmlDoc = new XmlDocument();
            XmlNode root = xmlDoc.AppendChild(xmlDoc.CreateElement("Include", "http://schemas.microsoft.com/wix/2006/wi"));

            XmlNode dir = root.AppendChild(xmlDoc.CreateElement("Directory", "http://schemas.microsoft.com/wix/2006/wi"));
            SetAttr(xmlDoc, dir, "Id", "TARGETDIR");
            SetAttr(xmlDoc, dir, "Name", "SourceDir");

            ProcessDirectory(xmlDoc, dir, folder);

            XmlNode feat = root.AppendChild(xmlDoc.CreateElement("Feature", "http://schemas.microsoft.com/wix/2006/wi"));
            SetAttr(xmlDoc, feat, "Id", "DefaultFeature");
            SetAttr(xmlDoc, feat, "Level", "1");
            SetAttr(xmlDoc, feat, "ConfigurableDirectory", "TARGETDIR");
            ProcessFeatures(xmlDoc, feat, dir);

            xmlDoc.Save(outputfile);
            return (0);
        }

        static void SetAttr(XmlDocument doc, XmlNode node, string name, string value)
        {
            XmlAttribute attr = node.Attributes.Append(doc.CreateAttribute(name));
            attr.Value = value;
        }

        static void ProcessDirectory(XmlDocument xmlDoc, XmlNode target, string directory)
        {
            string[] files = Directory.GetFiles(directory);
            string[] dirs = Directory.GetDirectories(directory);

            foreach (string file in files)
            {
                FileInfo finfo = new FileInfo(file);

                XmlNode comp = target.AppendChild(xmlDoc.CreateElement("Component", "http://schemas.microsoft.com/wix/2006/wi"));
                string guid = System.Guid.NewGuid().ToString().ToUpper();
                string id = "C_" + guid.Replace("-", "");
                SetAttr(xmlDoc, comp, "Id", id);
                SetAttr(xmlDoc, comp, "Guid", "{" + guid + "}");

                XmlNode fnode = comp.AppendChild(xmlDoc.CreateElement("File", "http://schemas.microsoft.com/wix/2006/wi"));
                SetAttr(xmlDoc, fnode, "Id", "F_" + guid.Replace("-",""));
                SetAttr(xmlDoc, fnode, "Name", finfo.Name);
                SetAttr(xmlDoc, fnode, "KeyPath", "yes");
                SetAttr(xmlDoc, fnode, "DiskId", "1");
                SetAttr(xmlDoc, fnode, "Source", finfo.FullName);

                if (finfo.Extension.ToLower() == ".dll" || finfo.Extension.ToLower() == ".exe")
                {
                    if (IsDotNetAssembly(finfo.FullName))
                    {
                        SetAttr(xmlDoc, fnode, "Assembly", ".net");
                        SetAttr(xmlDoc, fnode, "AssemblyManifest", "F_" + guid.Replace("-", ""));
                        SetAttr(xmlDoc, fnode, "AssemblyApplication", "F_" + guid.Replace("-", ""));
                    }
                }
            }

            foreach (string dir in dirs)
            {
                DirectoryInfo dinfo = new DirectoryInfo(dir);

                XmlNode dnode = target.AppendChild(xmlDoc.CreateElement("Directory", "http://schemas.microsoft.com/wix/2006/wi"));
                SetAttr(xmlDoc, dnode, "Id", dinfo.Name.Replace("-",""));
                SetAttr(xmlDoc, dnode, "Name", dinfo.Name);

                ProcessDirectory(xmlDoc, dnode, dinfo.FullName);
            }
        }

        static void ProcessFeatures(XmlDocument xmlDoc, XmlNode target, XmlNode root)
        {
            XmlNamespaceManager xmlmgr = new XmlNamespaceManager(xmlDoc.NameTable);
            xmlmgr.AddNamespace("wi", "http://schemas.microsoft.com/wix/2006/wi");
            XmlNodeList comps = root.SelectNodes("descendant::wi:Component", xmlmgr);

            foreach (XmlNode node in comps)
            {
                XmlNode compfeat = target.AppendChild(xmlDoc.CreateElement("ComponentRef", "http://schemas.microsoft.com/wix/2006/wi"));
                SetAttr(xmlDoc, compfeat, "Id", node.Attributes.GetNamedItem("Id").Value);
                XmlNode file = node.SelectSingleNode("wi:File", xmlmgr);
                if (file.Attributes.GetNamedItem("Assembly") != null)
                {
                    SetAttr(xmlDoc, compfeat, "Primary", "yes");
                }
            }
        }

        static bool IsDotNetAssembly(string fileName)
        {
            using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read))
            {
                try
                {
                    using (BinaryReader binReader = new BinaryReader(fs))
                    {
                        try
                        {
                            fs.Position = 0x3C; //PE Header start offset
                            uint headerOffset = binReader.ReadUInt32();
                                                         fs.Position = headerOffset + 0x18;
                            UInt16 magicNumber = binReader.ReadUInt16();
                                                         int dictionaryOffset;
                            switch (magicNumber)
                            {
                                case 0x010B: dictionaryOffset = 0x60; break;
                                case 0x020B: dictionaryOffset = 0x70; break;
                                default:
                                    throw new Exception("Invalid Image Format");
                            }

                            //position to RVA 15
                            fs.Position = headerOffset + 0x18 + dictionaryOffset + 0x70;

                            //Read the value
                            uint rva15value = binReader.ReadUInt32();
                            return (rva15value != 0);
                        }
                        finally
                        {
                            binReader.Close();
                        }
                    }
                }
                finally
                {
                    fs.Close();
                }
            }
        }
    }
}