Thursday, May 26, 2011

Build 32- and 64-bit installers using WiX

Summary: Code samples illustrating how to build deployment packages for both x86 and x64 platforms from the same Windows Installer XML (WiX) project.
This is post #6 of the six-part Learning WiX series. For other topics, see:

Table of contents
  1. Background (why WiX?)
  2. Introduction (answers to common questions)
  3. Getting started (references and tutorials)
  4. How-to's and missing links
  5. Slides and demo projects
  6. Improved demo projects (32- and 64-bit installers)
My recent post offers code samples illustrating a life cycle and core features of a WiX deployment project. The samples miss one important aspect: target platforms (i.e. x86 vs x64). While the samples get installed and run fine on both 64-bit and 32-bit platforms, the deployed applications always appear as 32-bit programs, even though they run as 64-bit processes on 64-bit machines (all assemblies are compiled to run on Any CPU).

I tried to add 64-bit support to the existing projects, but ran into several issues (e.g. folder selection wizard always showed the wrong Program Files folder on 64-bit systems). It took me a few weeks to resolve the problems, so here are the updated projects (all projects use Visual Studio 2010):
These samples do everything the original samples do, plus:
  • Solutions include build targets for 64-bit platforms: Debug (x64) and Release (x64).
  • WiX projects can build 64-bit MSI files (via 64-bit build targets).
  • Build process renames MSI files to indicate target platforms and copies them to a separate MSI folder under the WiX project (in a post-build event).
  • Detect and use the original application folder during upgrades.
Here is what you need to do to build installers for 32- and 64-bit platforms:
  • Understand the differences between 32- and 64-bit installers.
    At the very least, you need to understand that a setup package (MSI file) file can be marked as either a 32- or 64-bit installer (64-bit installer cannot run on 32-bit systems, but it can install 32-bit components on 64-bit systems). The following article can give you a basic idea of intricacies related to 64-bit platform deployment: How Windows Installer Processes Packages on 64-bit Windows (see also other relevant articles).

  • Add the x64 build target to your WiX setup project.
    Assuming that all application assemblies (and support files) are not platform-specific, keep their build configuration marked as Any CPU, but add new configurations to the WiX setup project and associate it with the x64 platform; make sure configuration names identify the 64-bit platform, e.g. Debug (x64) and Release (x64).

    Tip: I often run into problems adding a new configuration via Visual Studio IDE. E.g. sometimes, I cannot add the x64 platform to some projects, or I would add it, but when I close the Configuration Manager dialog box, my settings would disappear. If you run into such issues, I suggest making changes directly to the project (.wixproj) and solution (.sln) files (their structure should be obvious). [Notice that you can edit a project file directly in the Visual Studio IDE by unloading and reloading the project.] Then open the Configuration Manager dialog box (via the Build - Configuration Manager menu), and make sure your project build mappings looks right.
    Your configuration settings should look similar to this:

    The idea here is that you always build your platform-independent assemblies for Any CPU and only use x86 and x64 targets for the setup projects (or any platform-specific project).

  • Define and use platform-specific properties in the WiX source (.wxs) file.
    It's a good idea to have platform-specific GUIDs for product ID and upgrade code, as well as product name. You should always use a variable to store platform-specific Win64 flag and folder names (such as Program Files folder). Here is the code that illustrates how to achieve this:
    <?define ProductName = "WiX Demo" ?>
    <?define ProductVersion = "1.0" ?>
    <?define ProductFullVersion = "" ?>
    <?define ProductAuthor = "Alek Davis" ?>
    <?define ProductAppFolder = "InstallLocation" ?>
    <?if $(var.Platform) = x64 ?>
      <?define ProductDisplayName = "$(var.ProductName) 64-bit" ?>
      <?define ProductId = "47861F89-765F-4D6D-BEDE-139F0BCD74ED" ?>
      <?define ProductUpgradeCode = "EE2511C1-75A7-4954-8AB6-0E405C9481B4" ?>
      <?define Win64 = "yes" ?>
      <?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
    <?else ?>
      <?define ProductDisplayName = "$(var.ProductName)" ?>
      <?define ProductId = "490CCCF1-54C3-4AC2-8C88-A8903556EEB3" ?>
      <?define ProductUpgradeCode = "E7E6A7CB-1D12-486D-9E53-DBC56B0EDDCB" ?>
      <?define Win64 = "no" ?>
      <?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
    <?endif ?>
    The code above tells the WiX compiler to check the value of the build platform property $(var.Platform). If the compiler detects the x64 build platform target, it will set platform-dependent variables to 64-bit specific values; otherwise, it'll use 32-bit values.

  • Use platform-specific properties to set element attributes.
    Now you can use platform-specific properties set by WiX compiler instead of hard-coded values:
      Name="$(var.ProductDisplayName) (v$(var.ProductVersion))" 
        Platform="$(var.Platform)" />
      <Directory Id="TARGETDIR" Name="SourceDir">
        <Directory Id="$(var.PlatformProgramFilesFolder)" >
          <Directory Id="APPLICATIONFOLDER" Name="$(var.ProductName)"/>
  • Set platform flag on components.
    Make sure that all of your product components are marked with the appropriate Win64 flag. Use a variable (like $(var.Win64) defined in the code sample above) to change the value dynamically based on the build platform, such as:
    If you have components which must be deployed only on 32- or 64-bit platform, you can hard-code their Win64 attribute values and conditionally include or exclude them based on the build target:
    <?if $(var.Platform) = x64 ?>
      <!-- 64-bit components go here -->
    <?else ?>
      <!-- 32-bit components go here -->
    <?endif ?>
  • Rename MSI files to indicate target platform.
    You can define a post-build step to rename your MSI files, so that they reflect the intended platform. Select the Project - Properties menu; in the Build Events tab, set the Post-build Event Command Line to something like this:
    if not exist "$(ProjectDir)msi" mkdir  "$(ProjectDir)msi"
    copy "!(TargetPath)" "$(ProjectDir)msi\$(TargetName)($(PlatformName))$(TargetExt)" /Y /V
    These commands rename and copy the output MSI file to the MSI folder (in the project directory). They will create the folder if it does not exist. The file name will contain the (x86) or (x64) suffix depending in the target platform (e.g. WixDemo1.0(x64).msi).
That's about it. Oh, almost forgot: test, test, test...

See also:
Walking through the creation of a complex installer package by Gabriel Schenker


Paul said...

I am finishing up my combo wix installer - seems to be working. Thanks for the a good discussion of the topic. Tomorrow is full on testing. Don't forget alternate SystemFolder or System64Folder definitions if you are running Custom Actions with system utilities and would like to use the native version.

Unknown said...

It would be really helpful if you exported your project to VS 2008. Not all of us have enough money to replace our compilers every few years.

Alek Davis said...

That's actually a good suggestion. I'll do when I get some bandwidth. In the meantime, you can try a couple of approaches, like (a) use a Project Converter tool, or (b) just create an empty Visual Studio 8 solution and import the project files into it, as suggested here.

Unknown said...

Thanks Paul. Between your article and the wsx file I was able to figure it all out. Works great. I really appreaciated the section on forcing x86 and x64 for specific components. WIX absolutely kills InstallShield.

gr8dude said...

Thanks, this is exactly what I was looking for.

I got all the parts figured out, except that when I was using the platform-specific path in my custom actions or registry entries, they would used as "ProgramFilesFolder" instead of what path to the actual folder.

The solution is to use the []:

instead of what I was doing

Torben Schulz said...

Hi Alek,

thank you very much for this great post, it was exactly that what I was looking for and easy to understand.

Keep on :-)