faded picture of luke
a semi-random photo | click for the full photo gallery
click to browse photos
homepage navigation

Luke Melia

September 30, 2005

Wix fragment component generation

In order to get Wix to generate good MSIs, you can have no more than one DLL/EXE per component. In addition, each component needs it's own GUID. My team wanted a way to automate the definition of these components for our .NET apps as part of our automated build process. We tried using tallow/mallow, but that didn't really work out too well for us.

Here's the solution we came up, in a nutshell: a custom nant task runs against a build directory and catalogs all files in an XML file. Special logic applies for DLLs and EXEs, which get inspected and have relevant metadata added to thir elements in the XML. Once we have this XML, we apply an XSLT which transforms it into a Wix-friendly fragment file. We made one XSLT for executables, and another one for web services. We activate this XSLT as part of our nant script, and then included it in the call to Wix.

I'm including code listings for the relevant files here. It's available for use under a BSD license, which I'll include at the bottom.

The nant custom task: "BuildOutputToXmlTask.cs"

C#:
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Specialized;
  4. using System.Diagnostics;
  5. using System.IO;
  6. using System.Reflection;
  7. using System.Runtime.InteropServices;
  8. using System.Text;
  9. using System.Text.RegularExpressions;
  10. using System.Xml;
  11. using NAnt.Core;
  12. using NAnt.Core.Attributes;
  13. using NAnt.Core.Types;
  14.  
  15. namespace Oxygen.Build.Tasks
  16. {
  17.    /// <summary>
  18.    /// &amp;lt;buildoutputtoxml dir="build/app-client" output="build-output.xml" /&amp;gt;
  19.    /// </summary>
  20.    [TaskName("buildoutputtoxml")]
  21.    public class BuildOutputToXmlTask : Task
  22.    {
  23.       private string _dir;
  24.       private string _output = "build-output.xml";
  25.       private bool _debug;
  26.       private FileSet _fileset = new FileSet();
  27.       private const int MAX_PATH = 260;
  28.  
  29.       [TaskAttribute("dir", Required = true, ExpandProperties=true)]
  30.       [StringValidator(AllowEmpty = false)]
  31.       public string Dir
  32.       {
  33.          get { return _dir; }
  34.          set { _dir = value.Replace('/', '\\').TrimEnd(new char[] {'\\'}); }
  35.       }
  36.  
  37.       [TaskAttribute("output", Required = false)]
  38.       [StringValidator(AllowEmpty = false)]
  39.       public string Output
  40.       {
  41.          get { return _output; }
  42.          set { _output = value; }
  43.       }
  44.  
  45.       [TaskAttribute("debug", Required = false)]
  46.       [BooleanValidator]
  47.       public bool Debug
  48.       {
  49.          get { return _debug; }
  50.          set { _debug = value; }
  51.       }
  52.  
  53.       /// <summary>
  54.       /// Used to select the files to create XML for.
  55.       /// </summary>
  56.       [BuildElement("fileset")]
  57.       public virtual FileSet BuildOutputFileSet
  58.       {
  59.          get { return _fileset; }
  60.          set { _fileset = value; }
  61.       }
  62.  
  63.       protected override void ExecuteTask()
  64.       {
  65.          if (this.Debug) Debugger.Break();
  66.          //Check the directory exists
  67.          if (!Directory.Exists(this.Dir))
  68.             throw new BuildException("The directory specified does not exist");
  69.  
  70.          // ensure base directory is set, even if fileset was not initialized
  71.          // from XML
  72.          if (BuildOutputFileSet.BaseDirectory == null)
  73.          {
  74.             BuildOutputFileSet.BaseDirectory = new DirectoryInfo(Project.BaseDirectory);
  75.          }
  76.          if (BuildOutputFileSet.Includes.Count == 0)
  77.             BuildOutputFileSet.Includes.Add("**/*");
  78.  
  79.          XmlDocument xmlDoc = new XmlDocument();
  80.          XmlNode parentNode = xmlDoc.AppendChild(xmlDoc.CreateElement("buildOutput"));
  81.          AddFilesToXml(GetFileSetFilesInDir(this.Dir, false), xmlDoc, parentNode);
  82.          AddDirectoriesToXml(Directory.GetDirectories(this.Dir), xmlDoc, parentNode);
  83.          Project.Log(Level.Info, "Writing " + this.Output + " with contents of " + this.Dir);
  84.          XmlTextWriter writer = new XmlTextWriter(this.Output, Encoding.UTF8);
  85.          try
  86.          {
  87.             writer.Formatting = Formatting.Indented;
  88.             xmlDoc.WriteTo(writer);
  89.          }
  90.          finally
  91.          {
  92.             writer.Close();
  93.          }
  94.       }
  95.  
  96.       private void AddDirectoriesToXml(string[] directories, XmlDocument xmlDoc, XmlNode parentNode)
  97.       {
  98.          foreach (string subDir in directories)
  99.          {
  100.             if (DirectoryContainsFileSetFiles(subDir))
  101.             {
  102.                string directoryName = subDir.Replace(this.Dir + "\\", String.Empty);
  103.                string shortName = Path.GetFileName(GetShortPathName(subDir));
  104.                XmlElement directoryElement = xmlDoc.CreateElement("directory");
  105.                directoryElement.Attributes.Append(xmlDoc.CreateAttribute("shortName")).Value = shortName;
  106.                directoryElement.Attributes.Append(xmlDoc.CreateAttribute("longName")).Value = directoryName;
  107.                directoryElement.Attributes.Append(xmlDoc.CreateAttribute("processedName")).Value = ProcessDirectoryName(directoryName);
  108.                parentNode.AppendChild(directoryElement);
  109.                AddFilesToXml(GetFileSetFilesInDir(subDir, false), xmlDoc, directoryElement);
  110.                AddDirectoriesToXml(Directory.GetDirectories(subDir), xmlDoc, directoryElement);
  111.             }
  112.          }
  113.       }
  114.  
  115.       private void AddFilesToXml(StringCollection files, XmlDocument xmlDoc, XmlNode parentNode)
  116.       {
  117.          SortedList shortNames = new SortedList(files.Count);
  118.          foreach (string path in files)
  119.          {
  120.             string fileName = new FileInfo(path).Name;
  121.             string shortName = Path.GetFileName(GetShortPathName(path));
  122.             // Check if 8.3 name exists
  123.             // HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem NtfsDisable8dot3NameCreation=1
  124.             if (!Regex.IsMatch(shortName, @"^\S{1,8}\.\S{0,3}$"))
  125.             {
  126.                shortName = GenerateShortName(fileName, shortNames);
  127.             }
  128.             shortNames.Add(shortName, null);
  129.  
  130.             XmlElement fileElement = xmlDoc.CreateElement("file");
  131.             fileElement.Attributes.Append(xmlDoc.CreateAttribute("name")).Value = shortName;
  132.             fileElement.Attributes.Append(xmlDoc.CreateAttribute("longName")).Value = Path.GetFileName(fileName);
  133.             fileElement.Attributes.Append(xmlDoc.CreateAttribute("extension")).Value = Path.GetExtension(fileName);
  134.             fileElement.Attributes.Append(xmlDoc.CreateAttribute("processedName")).Value = ProcessFilePath(path);
  135.             fileElement.Attributes.Append(xmlDoc.CreateAttribute("generatedId")).Value = Guid.NewGuid().ToString();
  136.             if (path.ToLower().EndsWith(".exe") || path.ToLower().EndsWith(".dll"))
  137.             {
  138.                try
  139.                {
  140.                   // LoadFile vs LoadFrom lets us load an assembly without resolving anything just
  141.                   // to examine it. So already loaded assemblies or duplicate assemblies don't matter
  142.                   AssemblyName assemblyName = AssemblyName.GetAssemblyName(Path.GetFullPath(path));
  143.                   XmlElement assemblyElement = xmlDoc.CreateElement("assembly");
  144.                   fileElement.AppendChild(assemblyElement);
  145.                   string[] nameParts = Regex.Split(assemblyName.FullName, @", *|=");
  146.                   assemblyElement.Attributes.Append(xmlDoc.CreateAttribute("fullName")).Value = assemblyName.FullName;
  147.                   assemblyElement.Attributes.Append(xmlDoc.CreateAttribute("name")).Value = nameParts[0];
  148.  
  149.                   for (int i = 1; i + 1 <nameParts.Length; i += 2)
  150.                   {
  151.                      string name = ToLowerFirstChar(nameParts[i]);
  152.                      string value = nameParts[i + 1];
  153.                      if (value != "null")
  154.                         assemblyElement.Attributes.Append(xmlDoc.CreateAttribute(name)).Value = value;
  155.                   }
  156.                }
  157.                catch (Exception ex)
  158.                {
  159.                   Console.Out.WriteLine(ex.ToString());
  160.                }
  161.             }
  162.             parentNode.AppendChild(fileElement);
  163.          }
  164.          Project.Log(Level.Info, "Writing " + this.Output + " with contents of " + this.Dir);
  165.          XmlTextWriter writer = new XmlTextWriter(this.Output, Encoding.UTF8);
  166.          try
  167.          {
  168.             writer.Formatting = Formatting.Indented;
  169.             xmlDoc.WriteTo(writer);
  170.          }
  171.          finally
  172.          {
  173.             writer.Close();
  174.          }
  175.       }
  176.  
  177.       private string ProcessFilePath(string fullPath)
  178.       {
  179.          string path = fullPath.Substring(fullPath.IndexOf(this.Dir) + this.Dir.Length + 1);
  180.          return StripDotsAndSlashesAndInterCap(path);
  181.       }
  182.  
  183.       private string ProcessDirectoryName(string directoryName)
  184.       {
  185.          return StripDotsAndSlashesAndInterCap(directoryName);
  186.       }
  187.  
  188.       private string StripDotsAndSlashesAndInterCap(string s)
  189.       {
  190.          StringBuilder buffer = new StringBuilder();
  191.          bool lastCharWasPeriodOrBackslash = false;
  192.          for (int i = 0; i <s.Length; i++)
  193.          {
  194.             char c = s[i];
  195.             if ((c == '.') || (c == '\\'))
  196.             {
  197.                lastCharWasPeriodOrBackslash = true;
  198.             }
  199.             else
  200.             {
  201.                if (lastCharWasPeriodOrBackslash || i == 0)
  202.                   buffer.Append(Char.ToUpper(c));
  203.                else
  204.                   buffer.Append(c);
  205.                lastCharWasPeriodOrBackslash = false;
  206.             }
  207.          }
  208.          return buffer.ToString();
  209.       }
  210.  
  211.       private static string ToLowerFirstChar(string input)
  212.       {
  213.          if (input == null) return null;
  214.          if (input.Length == 0) return input;
  215.          if (input.Length == 1) return input.ToLower();
  216.          char[] output = new char[input.Length];
  217.          output[0] = Char.ToLower(input[0]);
  218.          input.CopyTo(1, output, 1, input.Length - 1);
  219.          return new string(output);
  220.       }
  221.  
  222.       /// <summary>
  223.       /// Gets the short name for a file.
  224.       /// </summary>
  225.       /// <param name="fullPath">Fullpath to file on disk.</param>
  226.       /// <returns>Short name for file.</returns>
  227.       private static string GetShortPathName(string fullPath)
  228.       {
  229.          StringBuilder shortPath = new StringBuilder(MAX_PATH, MAX_PATH);
  230.  
  231.          uint result = GetShortPathName(fullPath, shortPath, MAX_PATH);
  232.  
  233.          if (0 == result)
  234.          {
  235.             int err = Marshal.GetLastWin32Error();
  236.             throw new COMException("Failed to get short path name", err);
  237.          }
  238.  
  239.          return shortPath.ToString();
  240.       }
  241.  
  242.       /// <summary>
  243.       /// Gets the short name for a file.
  244.       /// </summary>
  245.       /// <param name="longPath">Long path to convert to short path.</param>
  246.       /// <param name="shortPath">Short path from long path.</param>
  247.       /// <param name="buffer">Size of short path.</param>
  248.       /// <returns>zero if success.</returns>
  249.       [DllImport("kernel32.dll", EntryPoint="GetShortPathNameW", CharSet=CharSet.Unicode, ExactSpelling=true, SetLastError=true)]
  250.       internal static extern uint GetShortPathName(string longPath, StringBuilder shortPath, [MarshalAs(UnmanagedType.U4)] int buffer);
  251.  
  252.       private string GenerateShortName(string name, SortedList shortNames)
  253.       {
  254.          int i = 1;
  255.          string shortName;
  256.          do
  257.          {
  258.             string extension = Path.GetExtension(name).ToUpper();
  259.             if (extension.Length> 4)
  260.                extension = extension.Substring(0, 4);
  261.             string baseName = Path.GetFileNameWithoutExtension(name).ToUpper();
  262.             if (baseName.Length> 6)
  263.                baseName = baseName.Substring(0, 6);
  264.             if (i <5) // win 2003 this is <5 for speed