WiX Auto-generation without using XLST

A quick recap: WiX (Windows Installer XML) is as the name implies XML based system of building installers. You gather your bunch of XML files that describe how you want the installer to look, function, etc, and when compiled with WiX, out pops an MSI. WiX is pretty much the defacto installer generation library - ClickOnce is balls, VS Installer is deprecated, and many other options either cost a fortune or simply just aren't as robust as WiX. While WiX is super powerful, it does have a bit of a learning curve which has given it a bit of a stigma.

I'm not too full of myself to say there are certain techs I suck at and don't want to learn. XLS/T is one of them. WiX (Windows Installer XML) is as the name implies XML based, and unfortunately that also means using XLST to transform any of the auto generated content from Heat (the "Harvest" tool which takes a directory and spits out a WiX fragment) into something a bit more usable. That's not to say Heat's output is useless, just for my particular configuration it wasn't "right".

There were a few approaches I could have gone for

  • Learn XLST - that sounded painful
  • Use C# to transform the XML generated by Heat.exe - more reasonable but still seemed pointless
  • Use C# to just generate the XML from a directory, skipping Heat.

While I'm my approach would incur the wrath of Rob Mensching, by using C# - a language I'm familiar with, a language I can maintain, I get a more robust install process.

As mentioned, WiX is all XML. .NET has a few ways to work with XML, but the absolute best? Linq-to-XML (XLinq).

static void Main(string[] args)
{
    var input = args[0];
    var output = args[1];

    XNamespace ns = "http://schemas.microsoft.com/wix/2006/wi";
    var document = new XDocument();
    var files = Directory.GetFiles(input).Select(x => new FileInfo(x));
    var diretories = Directory.GetDirectories(input).Select(x => new DirectoryInfo(x));
    var i = 1;
    document.Add(
        new XElement(ns + "Wix",
            new XElement(ns + "Fragment",
                new XElement(ns + "DirectoryRef",
                new XAttribute("Id", "INSTALLLOCATION"),
                    new XElement(ns + "Component",
                    new XAttribute("Id", "MahChatsFiles"),
                    new XAttribute("Guid", "4ac9e15b-89c1-4fde-84c9-286ef9d24af8"),
                    new XElement(ns + "RegistryKey",
                        new XAttribute("Root", "HKCU"),
                        new XAttribute("Key", @"Software\MahApps\MahChats"),
                        new XAttribute("Action", "createAndRemoveOnUninstall"),
                            new XElement(ns + "RegistryValue",
                                new XAttribute("Name", "Version"),
                                new XAttribute("Value", "[ProductVersion]"),
                                new XAttribute("Type", "string"),
                                new XAttribute("KeyPath", "yes"))),
                    new XElement(ns + "CreateFolder"),
                        new XElement(ns + "RemoveFolder",
                            new XAttribute("Id", "RemoveAppRootDirectory"),
                            new XAttribute("On", "uninstall")),
                            files.Select(x => new XElement(ns + "File",
                                new XAttribute("Id", x.Name.Replace('-', '_')),
                                new XAttribute("Source", x.FullName)))
                            ),
                            diretories.Select(x => new XElement(ns + "Directory",
                                new XAttribute("Id", x.Name.ToUpper()),
                                new XAttribute("Name", x.Name),
                                new XElement(ns + "Component",
                                    new XAttribute("Id", "ProductComponent" + i),
                                    new XAttribute("Guid", Guid.NewGuid()),
                                    new XElement(ns + "RegistryKey",
                                    new XAttribute("Root", "HKCU"),
                                    new XAttribute("Key", @"Software\MahApps\MahChats"),
                                    new XAttribute("Action", "createAndRemoveOnUninstall"),
                                        new XElement(ns + "RegistryValue",
                                            new XAttribute("Name", "Version"),
                                            new XAttribute("Value", "[ProductVersion]"),
                                            new XAttribute("Type", "string"),
                                            new XAttribute("KeyPath", "yes"))),
                                     new XElement(ns + "RemoveFolder",
                                        new XAttribute("Id", "ProductComponent" + i++),
                                        new XAttribute("On", "uninstall")),
                                    x.GetFiles().Select(y => new XElement(ns + "File",
                                        new XAttribute("Id", y.Name),
                                        new XAttribute("Source", y.FullName)))))
                                    )))));

    document.Save(output);
}

I'm sure many of you might balk at this gigantic statement, but (to me) it's more readable than XLST. It is a very specific query - for MahChats I'm toying with installing in a per-user, non-UAC required environment, like ClickOnce. That means it gets installed into C:\Users\User\AppData\Local\MahChats, and because it doesn't require elevation/is installed to that specific directory, I need to add in some registry keys and extra information.

The ultimate goal of this was to have the setup being generated by TeamCity as part of the CI build process. Now my build process looks like

  • Build project using Visual Studio Solution file
  • Use my "BurnTransformer" (specifically, BurnTransformer.exe %teamcity.build.checkoutDir%/src/MahChats/Bin/Release/ %teamcity.build.checkoutDir%\src\MahChats.Setup\fragment.wxs)
  • Build WiX project using a different Visual Studio Solution file
  • copy artifacts from src\MahChats.Setup\bin\**\*.msi

Why not ClickOnce which is "easier" and I've covered before? It's evil. Next month I hope to put out a "short horror story" on how ClickOnce is evil, and how to get similar installer experience (the good bits, that is) with WiX and other alternatives.

This means that yes, MahChats Alpha is getting closer. I still need a good/easy way to do updates, and I'm still in the process of setting up a custom NuGet server for (optional) plugins. On the installer front, I really want to take a look at WiX 3.6 which is currently in beta, as it has a way to do custom bootstrapper GUI's in WPF!

blog comments powered by Disqus