Building a Setup with Custom Actions using WiX

WiX[1] is a toolset that allows you to build setups for Windows Installer. Basically you write one or more XML files and then compile them using the compiler and linker from WiX. One of the reasons for using WiX is that it allows you to customize everything about the install. E.g.: when installing a plugin or update you need to find the folder on the target system that contains the app that needs to be extended but the unfortunately using the standard Setup and Deployment project in Visual Studio will only allow you to execute Custom Action (like searching for a registry key) AFTER copying the executable or dll to the target machine. Not with WiX.

Let me show you how I created a setup that checks for the existence of .NET 2.0 and uses a Custom Action to determine the location of Windows Live Writer[2] as specified by the SDK by calling AssocQueryString[3] for the .wpost extension to install a plugin.

Calling AssocQueryString can only be done from within running code so you’ll have to create a dll that exports a function that does precisely that. Here’s my code:

 

// WLWPluginSetupCustomAction.cpp : Defines the entry point for the DLL application.
//

#include “stdafx.h”

#ifdef _MANAGED
#pragma managed(push, off)
#endif

BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved )
{
  return TRUE;
}

#ifdef _MANAGED
#pragma managed(pop)
#endif

HRESULT GetPluginFolder(LPWSTR folder, DWORD size)
{
  ASSOCF flags = 0;
  ASSOCSTR str = ASSOCSTR_EXECUTABLE;
  WCHAR szOut[MAX_PATH];
  DWORD dwOut = ARRAYSIZE(szOut);

  HRESULT r;
  if(SUCCEEDED(r = AssocQueryString(flags, str, TEXT(“.wpost”), NULL, szOut, &dwOut)))
  {
    // find last backslash
    WCHAR *slash = _tcsrchr(szOut, TEXT(”));

    // remove windowslivewriter.exe from the path
    slash[1] = TEXT(”);

    // concatenate the name of the Plugins folder
    if(SUCCEEDED(r = StringCbCat(szOut, MAX_PATH, TEXT(“Plugins”))))
    {
      size_t l;
      if(SUCCEEDED(r = StringCbLength(szOut, MAX_PATH, &l)))
      {
        r = StringCbCopy(folder, l + sizeof(TCHAR), szOut);
      }
    }
  }
  return r;
}

UINT __stdcall GetWLWPluginFolder( MSIHANDLE hModule )
{
  WCHAR folder[MAX_PATH];
  HRESULT r;
  if(SUCCEEDED(r = GetPluginFolder(folder, MAX_PATH)))
  {
    if(SUCCEEDED(r = MsiSetProperty(hModule, TEXT(“WLWPluginsFolder”), folder)))
    {
      return ERROR_SUCCESS;
    }
  }
  return ERROR_INSTALL_FAILURE;
}

 

To inject this dll into the MSI setup file there are a couple of special tags you can use in a WiX file:

<?xml version=”1.0″ encoding=”UTF-8″?>
<Wix http://schemas.microsoft.com/wix/2006/wi&quot;”>http://schemas.microsoft.com/wix/2006/wi”>

&nbsp; <Product Manufacturer=”Airknow” Version=”2.0″ Name=”Community Server Gallery Plugin for Windows Live
&nbsp;&nbsp;&nbsp; Writer”
&nbsp;&nbsp;&nbsp; UpgradeCode=”YOURGUID-B5B9-425d-BDDE-708D651F67B2″
&nbsp;&nbsp;&nbsp; Id=”YOURGUID-7FE5-4406-AAE1-C5E407751335″ Language=”1033″>

&nbsp;&nbsp;&nbsp; <Package Comments=”Windows Live Writer and .NET 2.0 are required.” Compressed=”yes”
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; InstallerVersion=”200″ ShortNames=”no”
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Description=”This plugin enables Windows Live Writer to access the Community Server image
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; galleries. Images can be
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; uploaded&nbsp;and&nbsp;inserted into posts written in Windows Live Writer.” Manufacturer=”Airknow”/>

&nbsp; &nbsp;&nbsp;&nbsp;<Binary Id=’GETWLWFOLDER’
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; SourceFile=’..componentsWLWPluginSetupCustomActionWLWPluginSetupCustomAction.dll’/>
&nbsp;&nbsp;&nbsp; &nbsp;<CustomAction Id=’FindWLW’ BinaryKey=’GETWLWFOLDER’ DllEntry=’GetWLWPluginFolder’
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;Execute=’immediate’ />
&nbsp;&nbsp;&nbsp; <Media Id=’1′ Cabinet=’product.cab’ EmbedCab=’yes’ />

&nbsp;&nbsp;&nbsp; <Condition Message=’To use this plugin .NET 2.0 must be installed first.’ >
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <![CDATA[MsiNetAssemblySupport >= “2.0.50727”]]>
&nbsp;&nbsp;&nbsp; </Condition>
&nbsp;&nbsp; &nbsp;<Condition Message=’To use this plugin Windows Live Writer must be installed first.’ >
&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;<![CDATA[WLWPluginsFolder <> “”]]>
&nbsp;&nbsp;&nbsp; </Condition>

&nbsp;&nbsp;&nbsp; <Directory Id=”TARGETDIR” Name=”SourceDir”>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;<Directory Id=”WLWPluginsFolder”>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; <Component DiskId=”1″ Id=”CSGalleryPlugin” Guid=”YOURGUID-A977-4290-8ADE-81E45F0ABA2F”>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp; <File Id=”PluginFile” Name=”CSGalleryPlugin.dll”
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Source=”..componentsCSGalleryPluginCSGalleryPlugin.dll”/>
&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </Component>
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp; </Directory>
&nbsp;&nbsp; &nbsp;</Directory>

&nbsp;&nbsp;&nbsp; <Feature Id=’MyFeature’ Title=’The plugin’ Level=’1′>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <ComponentRef Id=’CSGalleryPlugin’ />
&nbsp;&nbsp; &nbsp;</Feature>

&nbsp;&nbsp;&nbsp; <InstallUISequence>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <Custom Action=’FindWLW’ Before=’LaunchConditions’ />
&nbsp;&nbsp; &nbsp;</InstallUISequence>
&nbsp;&nbsp;&nbsp; <InstallExecuteSequence>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <Custom Action=’FindWLW’ Before=’LaunchConditions’ />
&nbsp;&nbsp; &nbsp;</InstallExecuteSequence>
&nbsp; </Product>
</Wix>

I’m still not sure why I need to add both InstallUISequence AND InstallExecuteSequence; if one of them is absent it doesn’t install but if both are present the custom action runs twice…

BTW: I used WiX 3.0, I couldn’t get my custom action to fire with WiX 2

If you want the full sources go to CodePlex[4]

[1] WiX
[2] Windows Live Writer
[3] AssocQueryString
[4] http://www.codeplex.com/Wiki/View.aspx?ProjectName…