kentie.net

Installing DirectX with Wix

Note: If you're targeting the version of DirectX that comes with the Windows 8 SDK, you should simply have your installer put relevant DirectX DLLs in the application's target directory. See here .

When trying to run a DirectX application on a computer other than the one you initially developed it on, you've probably had to deal with the program complaining about not being able to find DirectX related dll files. Although major DirectX releases (9, 10, 11) only take place rarely, features do get added through a version's lifespan, resulting in SDK versions such as 'June 2010' (end-user runtime, SDK). When building an application with a specific SDK version, your users will need the dll files (D3DX and compiler ones in particular) that belong to that specific release.

Although many programs (such as games) just include a complete DirectX redistributable installation and launch it as a separate step after the normal installation, there's a better way. Looking at the unpacked redistributable that comes with the SDK (C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\Redist), there's nearly 100MB of files that go back to 2005-era SDK releases; let's just install the ones that are required, seamlessly integrated in the main install.

DirectSetup

Thanksfully, Microsoft provides a simple API to do just that. The DirectSetup documentation page describes which files to distribute, and functions to copy/install the files. To summarize, the following files are always required:

What else is required depends on the SDK version you're using and which DirectX libraries your program builds on. For each SDK version, there are library files for:

The DirectSetup API is quite simple. It installs/updates DirectX (which shouldn't done by hand) and doesn't provide any means to remove files; you're not supposed to. Really only two functions matter: DirectXSetup which installs the files, and DirectXSetupSetCallback, which allows a callback function to be set to track installation progress. This callback function receives information on the stage of the setup process (initializing, copying, etc) and which file is being worked on.

The Wix project

So we know which files to install, and that there are functions meant to install them. Clearly, this'll be a job for a custom action, but lets look at the installer first. As a demo project, let's make a simple installer that just updates the graphics related components of DirectX to the June 2010 versions. First, a .wxs file that contains the DirectX components:

 
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
  <?define DXSRCDIR=$(env.DXSDK_DIR)redist\?>
  <Fragment>
    <ComponentGroup Id="DXRedist">
      <Component Directory="DXRedistDir" >
        <File Source="$(var.DXSRCDIR)DSetup.dll" />
      </Component>
      <Component Directory="DXRedistDir">
        <File Source="$(var.DXSRCDIR)DSetup32.dll" />
      </Component>
      <Component Directory="DXRedistDir" >
        <File Source="$(var.DXSRCDIR)dxdllreg_x86.cab" />
      </Component>
      <Component Directory="DXRedistDir" >
        <File Source="$(var.DXSRCDIR)dxupdate.cab" />
      </Component>
      <Component Directory="DXRedistDir" >
        <File Source="$(var.DXSRCDIR)Jun2010_D3DCompiler_43_x86.cab" />
      </Component>
      <Component Directory="DXRedistDir" >
        <File Source="$(var.DXSRCDIR)Jun2010_d3dx11_43_x86.cab" />
      </Component>
    </ComponentGroup>
  </Fragment>
</Wix>
 

Simple. Next, the main product.wxs file. I've added comments to clarify things that might not be 100% straightforward; see my page on Wix Tips & Tricks for info on the UAC shield, MajorUpgrade element, etc.

 
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
  <!-- var.PRODUCT is defined in the project settings -->
  <Product Id="*" Name="$(var.PRODUCT)" Language="1033" Version="1.0.0.0" Manufacturer="Marijn Kentie" UpgradeCode="d28e46b3-dc38-4882-99b1-6156e1967691">
    <!-- PerMachine to show UAC shield on install button -->
    <Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />
    <!-- Easy way to take care of upgrades, to use this Product Id MUST be '*' -->
    <MajorUpgrade  AllowSameVersionUpgrades="yes" DowngradeErrorMessage="A more recent version of $(var.PRODUCT) is already installed." />
    <Media Id="1" Cabinet="media1.cab" EmbedCab="yes" />
 
    <Directory Id="TARGETDIR" Name="SourceDir">
      <Directory Id="ProgramFilesFolder">
        <Directory Id="INSTALLLOCATION" Name="DirectX">
          <Directory Id="DXRedistDir" Name="DXRedist" />
        </Directory>
      </Directory>
    </Directory>
 
    <Feature Id="ProductFeature" Title="DirectX" Level="1">
      <ComponentGroupRef Id="DXRedist" />
    </Feature>
 
    <Binary SourceFile="$(var.DirectSetupCA.TargetPath)" Id="CADLL" />
    <!-- Deferred custom action to install DirectX -->
    <CustomAction Id="InstallDirectX" BinaryKey="CADLL" DllEntry="InstallDirectX" Impersonate="no" Execute="deferred"  />
    <!-- Set the DirectX redist directory as the InstallDirectX custom action's Custom Action Data -->
    <CustomAction Id="InstallDirectX.SetDirectory" Return="check" Property="InstallDirectX" Value="[DXRedistDir]" />
 
    <InstallExecuteSequence>
      <Custom Action = "InstallDirectX.SetDirectory" Before="InstallDirectX"/>
      <!-- Run DirectX CA ONLY when we're installing/repairing/upgrading-->
      <Custom Action="InstallDirectX" Before="InstallFinalize">NOT REMOVE</Custom>
    </InstallExecuteSequence>
 
    <!-- UI, make sure to add WixUIExtension as a reference -->
    <UIRef Id="WixUI_Mondo" />
    <Property Id="WIXUI_INSTALLDIR" Value="INSTALLLOCATION" />
  </Product>
</Wix>
 

The installer puts the DirectX files we just added in the 'DXRedist' subdirectory of the installation and calls the custom action (see below) install them. Note that this is a deferred custom action: it is run once the installation script has been compiled, which means after the user hits the 'Install' button. Deferred custom action don't have access to all the various MSI variables such as [INSTALLLOCATION]. Instead, each has access to one specific variable for that one custom action, its Custom Action Data. For the DirectX custom action, I set that piece of data to the location of the redist files. For custom actions that require more data, it's up to the custom action author to pull them out of the one variable; one way is to comma separate them.

The custom action

The Wix part of the equation is straightforward; the custom action less so. A DLL custom action is a straight Win32 DLL C++ project, of which the MSI calls functions. I added it to the same solution as the Wix installer, and added a project reference so it gets built with the installer, can be referenced by output path, etc. In our case, the installer calls the InstallDirectX function (see the CustomAction element above). That function looks like this:

extern "C" __declspec(dllexport) UINT InstallDirectX(MSIHANDLE hInstall)
{    
    ::hInstall = hInstall;
 
    //Get directory where dependency DLL files are installed, and load them manually before the system tries to do this.
    //FOR THIS TO WORK, THE DLLS MUST BE SET TO DELAY-LOADED IN THE PROJECT LINKER OPTIONS!   
    wchar_t szInstallDir[MAX_PATH];
    GetCustomActionData(hInstall, szInstallDir);
    wchar_t szDSetupDllPath[MAX_PATH];
    PathCombine(szDSetupDllPath,szInstallDir,L"dsetup.dll");
 
    HMODULE pDSetupDll = LoadLibrary(szDSetupDllPath);
    if(!pDSetupDll)
    {        
        return EXIT_FAILURE;
    }
 
    if (MsiGetMode(hInstall,MSIRUNMODE_SCHEDULED) == FALSE)
    {
        return EXIT_FAILURE; //Must be deferred CA
    }
 
    //Create MSI process status
    hActionRec = MsiCreateRecord(3);
    if(hActionRec == NULL)
    {
        return EXIT_FAILURE;
    }
 
    //Install DirectX
    DirectXSetupSetCallback(callback);
    INT result = DirectXSetup(0,szInstallDir,DSETUP_DIRECTX);    
    MsiCloseHandle(hActionRec);
    if(result != DSETUPERR_SUCCESS && result != DSETUPERR_SUCCESS_RESTART)
    {
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}
 

And the GetCustomActionData function which reads the Custom Action data we set in the wxs file:

/**
Read the Custom Action Data for this CA
*/
static bool GetCustomActionData(MSIHANDLE hInstall, wchar_t* pszOut)
{
    DWORD dwLength=MAX_PATH;
 
    if(MsiGetProperty(hInstall, L"CustomActionData", pszOut, &dwLength) != ERROR_SUCCESS)
    {	     
        return false;
    }
    return true;
}
 

Note that things get hairy straight away. When a custom action dll gets run, it exists in a vacuum; there is no (clean) way to have the installer extract other files with it. As such, we can't just have it place dsetup.dll next to the custom action dll in whatever temporary location gets used for that kind of stuff. So how do we call functions in that dll, which is the whole point of our custom action? If we just link to dsetup.lib like usually when a dll gets used, calling the custom action will immediately fail as dsetup.dll is nowhere to be found.

The answer lies in delay loading. By setting dsetup.dll as a delay-loaded library, the operating system loads it only once we actually try to call one of its functions, instead of at start-up. This means that internally, the LoadLibrary and GetProcAddress functions get used, like in situations where one might want to check if a dll exists and/or offers certain functionality. For cases where we don't need to manually check if GetProcAddress succeeds though, delay loading is much less annoying to use, as it's completely transparent.

delayload

Now for the solution to the dsetup.dll issue. As we know the file's location (it was passed as Custom Action Data), and it won't get loaded at start-up, we can just load it from that location ourselves! Then, calls into the dll can proceed as usual. The rest of the main custom action function is simple enough: a callback function gets set, and DirectX is installed.

Callback and progress

The callback function's role is to convert DirectX setup progress into something that gets shown in the installer. By sending MsiProcessMessage messages to the MSI, we can enlarge the process bar (INSTALLMESSAGE_PROGRESS), increase the amount of process in the bar (also INSTALLMESSAGE_PROGRESS), set a name for the current action (INSTALLMESSAGE_ACTIONSTART), and set extra information for the current action, such as a file name (INSTALLMESSAGE_ACTIONDATA). See here for an example that includes all of this. There's a few things at play here, though:

  1. The callback function only gives you info on which of the total amount of files is being copied, and that file's name. Hard to translate into a useful progress bar value which should, according to the example, have about one progress bar tick per byte.
  2. Even if we were to choose some approximate file size, say 1MB per DirectX file, we would need to add a second, non-deferred, custom action to initially resize the progress bar. This could be done by calling the DirectXSetup function with the DSETUP_TESTINSTALL flag and remembering the total number of files, but that does make things more complicated.
  3. The standard Wix 'Mondo' UI does not have an ACTIONDATA field; it doesn't show the file names during normal installation either. So using INSTALLMESSAGE_ACTIONDATA messages with the copied files' names would go unseen.

So as I can't show the file names, yet can't advance a progress bar while showing a course indication of progress ('copying...') either, a more pragmatic solution is to just set the name of the current installation stage to a combination of the DirectX setup stage and file name, if applicable. Not entirely correct, but it does show the user progress without changing the rest of the installer or making things too complex.

installing
installing2
static DWORD __stdcall callback(DWORD Reason, DWORD MsgType, char * pszMessage, char * pszName, void * pInfo)
{   
    static const char* pszInstalling = "Installing DirectX:";
    if(!hActionRec)
    {
        return IDOK;
    }
 
    if(Reason==DSETUP_CB_MSG_PROGRESS)
    {   
        char szDesc[MAX_PATH+50];
        const char* pszPhaseText = "";
 
        DSETUP_CB_PROGRESS* pProgress = reinterpret_cast<DSETUP_CB_PROGRESS*>(pInfo);
        switch(pProgress->dwPhase)
        {
        case DSETUP_INITIALIZING:
            pszPhaseText = "Initializing";
            break;
        case DSETUP_EXTRACTING:
            pszPhaseText = "Extracting";
            break;
        case DSETUP_COPYING:
            pszPhaseText = "Copying";
            break;
        case DSETUP_FINALIZING:
            pszPhaseText = "Finalizing";
            break;
        }        
 
        if((pProgress->dwPhase == DSETUP_COPYING || pProgress->dwPhase == DSETUP_EXTRACTING) && !pszName) //There are copy progress messages with no file name; worthless for us
        {
            return IDOK;
        }
 
        if(pszName)
        {
            sprintf_s(szDesc,_countof(szDesc),"%s %s %s (%d of %d)",pszInstalling,pszPhaseText,pszName,pProgress->dwInPhaseProgress,pProgress->dwInPhaseMaximum);            
        }
        else
        {
            sprintf_s(szDesc,_countof(szDesc),"%s %s",pszInstalling, pszPhaseText);
        }        
 
        //The standard Wix Mondo UI doesn't have an ActionData text label, so just change the action name
        MsiRecordSetStringA(hActionRec, 1, "InstallDX"); //Action name
        MsiRecordSetStringA(hActionRec, 2, szDesc); //Description       
        MsiProcessMessage(hInstall, INSTALLMESSAGE_ACTIONSTART, hActionRec);
    }
    return IDOK;
}
 

Final words

Note that the example generates two ICE warnings:
ICE60: The file DSetup32.dll is not a Font, and its version is not a companion file reference. It should have a language specified in the Language column.
ICE61: This product should remove only older versions of itself. The Maximum version is not less than the current product. (1.0.0.0 1.0.0.0)
Nothing serious, and I've added them both to the ignore list.

Finally, note that users might not have the (Visual C++ 2010) runtime you're using to compile with; it's best to statically link it into the custom action DLL, and then, of course, using a merge module to install the runtime for use with your product.

runtime selection

Download the example project. Make sure you've got the June 2010 DirectX SDK, or later, installed.

Created: Feb 12 2012
Modified: Feb 08 2013