EDIT (9/26/2011) : This article is a little bit old. The dates are misleading because they reflect when I uploaded my articles to this new server; they don’t reflect the true creation date. A much simpler solution (one that is used in practice actually) for integrating your build scripts with WiX can be found at: http://badecho.com/2011/09/automating-wix-with-msbuild/

I’ve recently been working on designing and implementing a new automated build process for our main products at the company I work for. This challenging, yet interesting, problem manifests itself to be one of many distinct facets: the tie into versioning/source control, the various differing development technologies utilized in the product’s development, and the actual packaging stage, among others.

My most current endeavor relating to my work with developing a new build process corresponds to the packaging stage. The previous solution for this step was accomplished via Installshield; a packaging tool that would find itself being used again by the new build process.

However, soon after implementing it into the new process, I found it to be severely lacking in what I envisioned the process to eventually be capable of. To be brief, it suffers in the field of automation and would prove to only be an inhibitor to the build system’s ideal functionality. That’s when I stumbled upon WiX. As of version 3.0, the project files fully support MSBuild; additionally, the toolset also offers a MSBuild targets file as well as a MSBuild tasks library.

The task library is frequently advertised and used as a response to prospective users’ queries regarding automation capability. Despite this, examples of their use are not readily available online. Even worse, the documentation fails to mention even half of the available functionality within the task file. This posts hopes to alleviate these two problems.

As you may have already guessed, the build system utilizes Team Build, and is thusly driven by stock and custom MSBuild tasks. Installshield comes equipped with its own targets file and tasks; however, to say they are even somewhat acceptable in terms of documentation available would be a stretch. WiX fares better in this area, and although the availability of tasks was not the primary reason for switching packaging technologies, it was something to look forward to.

WiX 3.0 documentation covers three specific tasks: Candle, Light, and Lit. These correspond to the various utility-oriented tools that come with the WiX toolset. After popping the WiX assembly into the .NET reflector, however, I was surprised to find even more tasks we could make use of. Because the other three tasks are already documented, we’re only going to cover the others.

Following the brief overview of each of these tasks, I’ve provided some skeleton code for integrating WiX into your Team Build/MSBuild project on a basic level.

CreateProjectReferenceDefineConstants Task

The disassembly of this task is shown below:

public sealed class CreateProjectReferenceDefineConstants : Task
{
// Fields
private ITaskItem[] defineConstants;
private ITaskItem[] projectConfigurations;
private ITaskItem[] projectReferencePaths;

// Methods
public CreateProjectReferenceDefineConstants();
public override bool Execute();
private string FindProjectConfiguration(string projectName);

// Properties
[Output]
public ITaskItem[] DefineConstants { get; }
public ITaskItem[] ProjectConfigurations { get; set; }
[Required]
public ITaskItem[] ProjectReferencePaths { get; set; }
}

This task allows us to create a list of preprocessor defines that will be passed between the referenced project and candle (Refer to the WiX manual for more information regarding preprocessors).

This task is useful, but slightly silly in one aspect: You need to supply paths to the WiX project file in two places. One is in the Include attribute of the task, and the other place is through the MSBuildSourceProjectFileFullPath. It is required that both be supplied with the path, or else the task fails. I fail to see the point of this, perhaps someone more knowledgeable out there can enlighten us to the point of this.

Each project reference in ProjectReferencePaths accepts the following elements as metadata:


<ProjectReferencePaths Include="Path\To\WixProj">
    <Configuration>
        <!-- Specify your Debug/Release build configuration conditionals here -->
        Debug
    </Configuration>
    <Platform>
        <!-- Specify the build platform here -->
        x86
    </Platform>
    <FullConfiguration>
        <!-- Or, you can forego the previous two and specify both here -->
        Debug|x86
    </FullConfiguration>
    <MSBuildSourceProjectFileFullPath>
        <!-- Used to specify the path to the .wixproj -->
        \Path\To\WixProj
    </MSBuildSourceProjectFileFullPath>
    <FullPath>
        <!-- Used to specify the full path to the target directory -->
        \Put\WixProj\Here
    </FullPath>
</ProjectReferencePaths>

The value of this task item should be apparent, as it provides a simple way to specify input and output directories on the fly, among other items.

If no configuration data is supplied via the above property, it can be supplied through the optional ProjectConfigurations property.

<ProjectConfigurations Include="Release|x86" />

Only supply it through this property if you have not supplied it through the FullConfiguration metadata, or by through the combination of the Configuration and Platform metadatas, in the ProjectReferencePath task item.

Once all is said and done, we can declare this task in our .proj file in order to pass the desired preprocessing variables to candle.

<CreateProjectReferenceDefineConstants
    ProjectReferencePaths = "@(ProjectReferencePaths)"
    ProjectConfigurations = "@(ProjectConfigurations)">
    <Output TaskParameter="DefineConstants"
        ItemName = "ItemToHoldOutput"
    />
</CreateProjectReferenceDefineConstants>

HeatTask Task

The disassembly for this task is shown below:

public abstract class HeatTask : WixToolTask
{
// Fields
private bool autogenerageGuids;
private bool generateGuidsNow;
private const string HeatToolName = "Heat.exe";
private ITaskItem outputFile;
private bool suppressFragments;
private bool suppressUniqueIds;

// Methods
protected HeatTask();
protected override void BuildCommandLine(WixCommandLineBuilder commandLineBuilder);
protected override string GenerateFullPathToTool();

// Properties
public bool AutogenerateGuids { get; set; }
public bool GenerateGuidsNow { get; set; }
protected abstract string OperationName { get; }
[Output, Required]
public ITaskItem OutputFile { get; set; }
public bool SuppressFragments { get; set; }
public bool SuppressUniqueIds { get; set; }
protected override string ToolName { get; }
}

This allows us to utilize the new (well, newer than Tallow at least) WiX utility of the name “heat.exe”, which is useful for creating an initial code skeleton for any directories of new items to add to the installation project.

Since this is basically a ToolTask, it is obvious that this is just a command line wrapper to the actual heat.exe, so because there is documentation and, of course, examples of its use out there in the wild, I don’t feel the need to have to demonstrate its use in MSBuild.

ReadRegistry/RegistryBase Tasks

The disassembly for ReadRegistry is shown below:

public class ReadRegistry : RegistryBase
{
// Fields
private bool failIfMissing;
private bool keyExists;
private bool nameExists;
private string regValue;

// Methods
public ReadRegistry();
public override bool Execute();

// Properties
public bool FailIfMissing { get; set; }
[Output]
public bool KeyExists { get; }
[Output]
public bool NameExists { get; }
[Output]
public string Value { get; }
}

…and the disassembly for RegistryBase can be viewed below here:

public abstract class RegistryBase : Task
{
// Fields
private RegistryHive hive;
private string key;
private string name;

// Methods
protected RegistryBase();

// Properties
[Required]
public string Hive { get; set; }
protected RegistryKey HiveRegistryKey { get; }
[Required]
public string Key { get; set; }
public string Name { get; set; }
}

We aren’t meant to actually use RegistryBase, but it is there if anyone feels like expanding on it. The ReadRegistry task, however, gives us the ability to read from the registry during build time. This task can also be set to fail or not fail if it does not find the specified key, this is up to you.

<ReadRegistry
    FailIfMissing = "false"
    Key = "SOFTWARE\Omni"
    Name = "keyName"
    Hive = "LocalMachine">
    <Output TaskParameter = "KeyExists"
        ItemName = "DoesKeyExist"
    />
    <Output TaskParameter = "NameExists"
        ItemName = "DoesNameExist"
    />
    <Output TaskParameter = "Value"
        ItemName = "ValueOfKey"
    />
</ReadRegistry>

Torch Task

The disassembly for this task is shown below:

public sealed class Torch : WixToolTask
{
// Fields
private ITaskItem baselineFile;
private bool inputIsXml;
private bool leaveTemporaryFiles;
private bool outputAsXml;
private ITaskItem outputFile;
private bool preserveUnmodifiedContent;
private const string TorchToolName = "Torch.exe";
private ITaskItem updateFile;

// Methods
public Torch();
protected override void BuildCommandLine(WixCommandLineBuilder commandLineBuilder);
protected override string GenerateFullPathToTool();

// Properties
[Required]
public ITaskItem BaselineFile { get; set; }
public bool InputIsXml { get; set; }
public bool LeaveTemporaryFiles { get; set; }
public bool OutputAsXml { get; set; }
[Output, Required]
public ITaskItem OutputFile { get; set; }
public bool PreserveUnmodifiedContent { get; set; }
protected override string ToolName { get; }
[Required]
public ITaskItem UpdateFile { get; set; }
}

This is a task that exposes use of another WiX utility, Torch. Because this is a well documented external program, I’m not going to go over it here; this task is key for on-demand patching, however.

WixAssignCulture Task

The disassembly is shown below:

public class WixAssignCulture : Task
{
// Fields
private List<ITaskItem> allFilesWithCulture;
private List<ITaskItem> assignedFiles;
private const string CultureAttributeName = "Culture";
private const string CultureMetadataName = "Culture";
private ITaskItem[] files;
private List<ITaskItem> skippedFilesWithExistingCulture;
private List<ITaskItem> skippedFilesWithNoCulture;

// Methods
public WixAssignCulture();
public override bool Execute();

// Properties
[Output]
public ITaskItem[] AllFilesWithCulture { get; }
[Required]
public ITaskItem[] Files { get; set; }
}

This task will assign Culture metadata to files depending on the value of the Culture attribute on the WixLocalization element inside the file.

It basically takes each file you provide via Files and calls the SetMetaData method (provided by the ITaskItem class) using the “Culture” for the metadata name and the culture value found within.

The only files we’d be using here are WiX localization files. What this does for us is collect the cultures from the provided files and returns the files with the correct localization metadata provided.
As much as I’d like to, I have not used this task yet, thus I would feel silly providing a real world example of its use. If I find myself using it someday, however, I’ll put something up for curious eyes.

WixToolTask Task

The disassembly is shown below:

public abstract class WixToolTask : ToolTask, IDisposable
{
// Fields
private string additionalOptions;
private bool disposed;
private int exitCode;
private Queue<string> messageQueue;
private ManualResetEvent messagesAvailable;
private bool noLogo;
private bool runAsSeparateProcess;
private bool suppressAllWarnings;
private string[] suppressSpecificWarnings;
private ManualResetEvent toolExited;
private string[] treatSpecificWarningsAsErrors;
private bool treatWarningsAsErrors;
private bool verboseOutput;

// Methods
protected WixToolTask();
protected static List<string> AdjustFilePaths(
ITaskItem[] tasks,
string[] referencePaths);
protected virtual void BuildCommandLine(WixCommandLineBuilder commandLineBuilder);
public void Dispose();
protected virtual void Dispose(bool disposing);
protected override int ExecuteTool(
string pathToTool,
string responseFileCommands,
string commandLineCommands);
private void ExecuteToolThread(object parameters);
protected override string GenerateResponseFileCommands();
private string GetTemporaryResponseFile(
string responseFileCommands,
out string responseFileSwitch);
private void HandleToolMessages();

// Properties
public string AdditionalOptions { get; set; }
public bool NoLogo { get; set; }
public bool RunAsSeparateProcess { get; set; }
public bool SuppressAllWarnings { get; set; }
public string[] SuppressSpecificWarnings { get; set; }
public string[] TreatSpecificWarningsAsErrors { get; set; }
public bool TreatWarningsAsErrors { get; set; }
public bool VerboseOutput { get; set; }

// Nested Types
private class WixToolTaskLogger : TextWriter
{
// Fields
private StringBuilder buffer;
private Queue<string> messageQueue;
private ManualResetEvent messagesAvailable;

// Methods
public WixToolTaskLogger(
Queue<string> messageQueue,
ManualResetEvent messagesAvailable);
public override void Write(char value);

// Properties
public override Encoding Encoding { get; }
}
}

This is used as a base for many of the tasks, and can be useful in deriving your own custom tasks for WiX. Some overridable methods are clearly only useful for internal WiX use, such as ExecuteTool, although I’m sure we can employ their use if we wanted to.

I have not created any custom tasks using this as a base as of yet, when I do I’ll slap ‘em here.

Example of using WiX in a Team Build/MSBuild Environment

After the compilation of the various areas of code for your product, you will want Team Build to build the packages to finish things off. Provided below are some snippets showing how to accomplish this within your preexisting Team Build/MSBuild project file.

.
.
<PropertyGroup>
    <!--You can define the OutputName property, which names the output .msi.-->
    <!--This will override what's in the .wixproj. Do not include the file extension. -->
    <OutputName>MyMsi</OutputName>
    <WixTargetsPath Condition=" '$(WixTargetsPath)' == '' ">
        $(MSBuildExtensionsPath)\Microsoft\WiX\v3.0\Wix.targets
    </WixTargetsPath>
</PropertyGroup>
.
.
<Import Project="$(WixTargetsPath)" />
.
.
<ItemGroup>
    <Projects Include="Path\To\WixProj"/>
    <ProjectReferencePaths Include="@(Projects)">
        <Configuration>Debug</Configuration>
        <Platform>x86</Platform>
        <MSBuildSourceProjectFileFullPath>@(Projects)</MSBuildSourceProjectFileFullPath>
    </ProjectReferencePaths>
</ItemGroup>
.
.
<!-- Set the target name to whatever you've designated as your post-build target, -->
<!-- of course. -->
<Target Name="AfterBuild">
    <CreateProjectReferenceDefineConstants
        ProjectReferencePaths="@(ProjectReferencePaths)"/>

    <!-- Feel free to supply additional properties here as well... -->
    <MSBuild Projects="@(Projects)"/>
</Target>

Placing the above in the appropriate places in your Team Build/MSBuild .proj file will result in the specified WiX projects to be built. Much more customization and/or additional criteria can be specified of course, and probably should be in order to give you the desired solution; however, this is a good enough “skeleton” to work off of.

Matt Weber

I'm the the Senior Software Architect at Emergingsoft where I lead the software development team. I am also the owner of this website. I enjoy well-designed code, independent thought, and the application of rationality in general. You can reach me at matt@badecho.com.

 Leave a Reply

(required)

(required)

You may use these HTML tags and attributes: <a href="" title="" rel=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

 
   
© 2012-2013 Matt Weber. All Rights Reserved. Terms of Use.