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"
-
using System;
-
using System.Collections;
-
using System.Collections.Specialized;
-
using System.Diagnostics;
-
using System.IO;
-
using System.Reflection;
-
using System.Runtime.InteropServices;
-
using System.Text;
-
using System.Text.RegularExpressions;
-
using System.Xml;
-
using NAnt.Core;
-
using NAnt.Core.Attributes;
-
using NAnt.Core.Types;
-
-
namespace Oxygen.Build.Tasks
-
{
-
/// <summary>
-
/// &lt;buildoutputtoxml dir="build/app-client" output="build-output.xml" /&gt;
-
/// </summary>
-
[TaskName("buildoutputtoxml")]
-
public class BuildOutputToXmlTask : Task
-
{
-
private string _dir;
-
private string _output = "build-output.xml";
-
private bool _debug;
-
private const int MAX_PATH = 260;
-
-
[TaskAttribute("dir", Required = true, ExpandProperties=true)]
-
[StringValidator(AllowEmpty = false)]
-
public string Dir
-
{
-
get { return _dir; }
-
}
-
-
[TaskAttribute("output", Required = false)]
-
[StringValidator(AllowEmpty = false)]
-
public string Output
-
{
-
get { return _output; }
-
set { _output = value; }
-
}
-
-
[TaskAttribute("debug", Required = false)]
-
[BooleanValidator]
-
public bool Debug
-
{
-
get { return _debug; }
-
set { _debug = value; }
-
}
-
-
/// <summary>
-
/// Used to select the files to create XML for.
-
/// </summary>
-
[BuildElement("fileset")]
-
public virtual FileSet BuildOutputFileSet
-
{
-
get { return _fileset; }
-
set { _fileset = value; }
-
}
-
-
protected override void ExecuteTask()
-
{
-
if (this.Debug) Debugger.Break();
-
//Check the directory exists
-
if (!Directory.Exists(this.Dir))
-
-
// ensure base directory is set, even if fileset was not initialized
-
// from XML
-
if (BuildOutputFileSet.BaseDirectory == null)
-
{
-
}
-
if (BuildOutputFileSet.Includes.Count == 0)
-
BuildOutputFileSet.Includes.Add("**/*");
-
-
XmlNode parentNode = xmlDoc.AppendChild(xmlDoc.CreateElement("buildOutput"));
-
AddFilesToXml(GetFileSetFilesInDir(this.Dir, false), xmlDoc, parentNode);
-
AddDirectoriesToXml(Directory.GetDirectories(this.Dir), xmlDoc, parentNode);
-
Project.Log(Level.Info, "Writing " + this.Output + " with contents of " + this.Dir);
-
try
-
{
-
writer.Formatting = Formatting.Indented;
-
xmlDoc.WriteTo(writer);
-
}
-
finally
-
{
-
writer.Close();
-
}
-
}
-
-
private void AddDirectoriesToXml(string[] directories, XmlDocument xmlDoc, XmlNode parentNode)
-
{
-
foreach (string subDir in directories)
-
{
-
if (DirectoryContainsFileSetFiles(subDir))
-
{
-
string directoryName = subDir.Replace(this.Dir + "\\", String.Empty);
-
string shortName = Path.GetFileName(GetShortPathName(subDir));
-
XmlElement directoryElement = xmlDoc.CreateElement("directory");
-
directoryElement.Attributes.Append(xmlDoc.CreateAttribute("shortName")).Value = shortName;
-
directoryElement.Attributes.Append(xmlDoc.CreateAttribute("longName")).Value = directoryName;
-
directoryElement.Attributes.Append(xmlDoc.CreateAttribute("processedName")).Value = ProcessDirectoryName(directoryName);
-
parentNode.AppendChild(directoryElement);
-
AddFilesToXml(GetFileSetFilesInDir(subDir, false), xmlDoc, directoryElement);
-
AddDirectoriesToXml(Directory.GetDirectories(subDir), xmlDoc, directoryElement);
-
}
-
}
-
}
-
-
private void AddFilesToXml(StringCollection files, XmlDocument xmlDoc, XmlNode parentNode)
-
{
-
foreach (string path in files)
-
{
-
string shortName = Path.GetFileName(GetShortPathName(path));
-
// Check if 8.3 name exists
-
// HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem NtfsDisable8dot3NameCreation=1
-
if (!Regex.IsMatch(shortName, @"^\S{1,8}\.\S{0,3}$"))
-
{
-
shortName = GenerateShortName(fileName, shortNames);
-
}
-
shortNames.Add(shortName, null);
-
-
XmlElement fileElement = xmlDoc.CreateElement("file");
-
fileElement.Attributes.Append(xmlDoc.CreateAttribute("name")).Value = shortName;
-
fileElement.Attributes.Append(xmlDoc.CreateAttribute("longName")).Value = Path.GetFileName(fileName);
-
fileElement.Attributes.Append(xmlDoc.CreateAttribute("extension")).Value = Path.GetExtension(fileName);
-
fileElement.Attributes.Append(xmlDoc.CreateAttribute("processedName")).Value = ProcessFilePath(path);
-
fileElement.Attributes.Append(xmlDoc.CreateAttribute("generatedId")).Value = Guid.NewGuid().ToString();
-
if (path.ToLower().EndsWith(".exe") || path.ToLower().EndsWith(".dll"))
-
{
-
try
-
{
-
// LoadFile vs LoadFrom lets us load an assembly without resolving anything just
-
// to examine it. So already loaded assemblies or duplicate assemblies don't matter
-
AssemblyName assemblyName = AssemblyName.GetAssemblyName(Path.GetFullPath(path));
-
XmlElement assemblyElement = xmlDoc.CreateElement("assembly");
-
fileElement.AppendChild(assemblyElement);
-
string[] nameParts = Regex.Split(assemblyName.FullName, @", *|=");
-
assemblyElement.Attributes.Append(xmlDoc.CreateAttribute("fullName")).Value = assemblyName.FullName;
-
assemblyElement.Attributes.Append(xmlDoc.CreateAttribute("name")).Value = nameParts[0];
-
-
for (int i = 1; i + 1 <nameParts.Length; i += 2)
-
{
-
string name = ToLowerFirstChar(nameParts[i]);
-
string value = nameParts[i + 1];
-
if (value != "null")
-
assemblyElement.Attributes.Append(xmlDoc.CreateAttribute(name)).Value = value;
-
}
-
}
-
catch (Exception ex)
-
{
-
Console.Out.WriteLine(ex.ToString());
-
}
-
}
-
parentNode.AppendChild(fileElement);
-
}
-
Project.Log(Level.Info, "Writing " + this.Output + " with contents of " + this.Dir);
-
try
-
{
-
writer.Formatting = Formatting.Indented;
-
xmlDoc.WriteTo(writer);
-
}
-
finally
-
{
-
writer.Close();
-
}
-
}
-
-
private string ProcessFilePath(string fullPath)
-
{
-
string path = fullPath.Substring(fullPath.IndexOf(this.Dir) + this.Dir.Length + 1);
-
return StripDotsAndSlashesAndInterCap(path);
-
}
-
-
private string ProcessDirectoryName(string directoryName)
-
{
-
return StripDotsAndSlashesAndInterCap(directoryName);
-
}
-
-
private string StripDotsAndSlashesAndInterCap(string s)
-
{
-
bool lastCharWasPeriodOrBackslash = false;
-
for (int i = 0; i <s.Length; i++)
-
{
-
char c = s[i];
-
if ((c == '.') || (c == '\\'))
-
{
-
lastCharWasPeriodOrBackslash = true;
-
}
-
else
-
{
-
if (lastCharWasPeriodOrBackslash || i == 0)
-
buffer.Append(Char.ToUpper(c));
-
else
-
buffer.Append(c);
-
lastCharWasPeriodOrBackslash = false;
-
}
-
}
-
return buffer.ToString();
-
}
-
-
private static string ToLowerFirstChar(string input)
-
{
-
if (input == null) return null;
-
if (input.Length == 0) return input;
-
if (input.Length == 1) return input.ToLower();
-
output[0] = Char.ToLower(input[0]);
-
input.CopyTo(1, output, 1, input.Length - 1);
-
}
-
-
/// <summary>
-
/// Gets the short name for a file.
-
/// </summary>
-
/// <param name="fullPath">Fullpath to file on disk.</param>
-
/// <returns>Short name for file.</returns>
-
private static string GetShortPathName(string fullPath)
-
{
-
-
uint result = GetShortPathName(fullPath, shortPath, MAX_PATH);
-
-
if (0 == result)
-
{
-
int err = Marshal.GetLastWin32Error();
-
}
-
-
return shortPath.ToString();
-
}
-
-
/// <summary>
-
/// Gets the short name for a file.
-
/// </summary>
-
/// <param name="longPath">Long path to convert to short path.</param>
-
/// <param name="shortPath">Short path from long path.</param>
-
/// <param name="buffer">Size of short path.</param>
-
/// <returns>zero if success.</returns>
-
[DllImport("kernel32.dll", EntryPoint="GetShortPathNameW", CharSet=CharSet.Unicode, ExactSpelling=true, SetLastError=true)]
-
internal static extern uint GetShortPathName(string longPath, StringBuilder shortPath, [MarshalAs(UnmanagedType.U4)] int buffer);
-
-
private string GenerateShortName(string name, SortedList shortNames)
-
{
-
int i = 1;
-
string shortName;
-
do
-
{
-
string extension = Path.GetExtension(name).ToUpper();
-
if (extension.Length> 4)
-
extension = extension.Substring(0, 4);
-
string baseName = Path.GetFileNameWithoutExtension(name).ToUpper();
-
if (baseName.Length> 6)
-
baseName = baseName.Substring(0, 6);
-
if (i <5) // win 2003 this is <5 for speed



