Saturday, May 04, 2013

WiX: Installing Files to Public Documents Folder

A few days ago I stumbled over the problem of installing files into the “Public Documents” folder using the Windows Installer Toolkit (WiX). Although the different bits a pieces are available somewhere on the internet I couldn’t locate a single source that describes a complete solution.

On Windows based systems each user has their own personal folder but there is also a folder called “Public Documents”. For the task at hand, I wanted to install a few sample text files into a subfolder of the public documents folder using WiX. For this example I used Visual Studio 2012 (Professional Edition or better is sufficient) with the Visual Studio extension installed by WiX 4.0.

Let’s start with the wizard for the WiX setup project:

image

As the name for the setup you can pick anything you like. In this case I chose “Acme.Setup”. The resulting project looks very simple. It has no references and only a single WiX script file in it:

image

The content of Produce.wxs is very basic (Note that the first few digits of all GUIDs in this post are replaced with the string ‘YOURGUID’. You need to replace the GUIDs with your own to make it work):

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
    <Product Id="*" Name="Acme.Setup" Language="1033" Version="1.0.0.0" Manufacturer="" UpgradeCode="YOURGUID-269e-4795-97d7-a392d75749fb">
        <Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />

        <MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
        <MediaTemplate />

        <Feature Id="ProductFeature" Title="Acme.Setup" Level="1">
            <ComponentGroupRef Id="ProductComponents" />
        </Feature>
    </Product>

    <Fragment>
        <Directory Id="TARGETDIR" Name="SourceDir">
            <Directory Id="ProgramFilesFolder">
                <Directory Id="INSTALLFOLDER" Name="Acme.Setup" />
            </Directory>
        </Directory>
    </Fragment>

    <Fragment>
        <ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
            <!-- TODO: Remove the comments around this Component element and the ComponentRef below in order to add resources to this installer. -->
            <!-- <Component Id="ProductComponent"> -->
                <!-- TODO: Insert files, registry keys, and other resources here. -->
            <!-- </Component> -->
        </ComponentGroup>
    </Fragment>
</Wix>

Obviously, we now need to change this to add some files. So let’s pick a text file from a different project in the same solution. In this case I created a C# library projects called Acme.Lib and added one simple text file. We can ignore the default class ‘Class1’ that is generated in the Acme.Lib project. It is not relevant for this blog post. Here is what the solution looks like now:

image

The content of the file readme.txt and it’s name are irrelevant as well. Choose what works best for your case.

Next we need to make sure that the installer script knows about the text file and where it is located. The first option is to use your knowledge about the location of the two projects, Acme.Lib and Acme.Setup. So you could change the file Product.wxs as follows:

<Fragment>
    <ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
        <!-- TODO: Remove the comments around this Component element and the ComponentRef below in order to add resources to this installer. -->
        <!-- <Component Id="ProductComponent"> -->
            <!-- TODO: Insert files, registry keys, and other resources here. -->
      <Component Id="ReadmeFile" Guid="{YOURGUID-994F-49C0-A7AC-53D1571A295A}">
         <File Id="readme.txt" Name="readme.txt" Source="..\Acme.Lib\readme.txt"></File>
      </Component>
        <!-- </Component> -->
    </ComponentGroup>
</Fragment>

Some points of interest. First of all we need to add a new <Component> to the script. Your installer can consist of thousands of components if you wish. In this case I’m only adding one component with an Id and a GUID. Again, please note that I have replaced the first few digits of the GUID with ‘YOURGUID’. You need to replace all GUIDs in this example with newly generated GUIDs anyways to make it work.

Inside of the component I place a <File> tag with the Id, the name and the Source of the file. The Id needs to be unique within the installer while the name is the name that the file will have once installed. The ‘Source’ attributes tells the WiX toolset where to find the file so it can be included into the installer. Note that in contrast to some other installer tools sets the build of the installer will fail in case the WiX toolset can’t find the file.

At this stage there one additional tweak that we have to do to compile this build script successfully. We need to edit the 'Manufacturer’ attribute at the beginning of Product.wxs. Here are the first four lines (Note that ‘Manufacturer’ has been set to ‘Acme’):

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
    <Product Id="*" Name="Acme.Setup" Language="1033" Version="1.0.0.0" Manufacturer="Acme" UpgradeCode="YOURGUID-269e-4795-97d7-a392d75749fb">
        <Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />

If you now build your solution it should build without problems.

At this stage all you have is an installer that installs into the into a subfolder in Program Files. It works but it is not what we want. So let’s have a look how we can change that.

The trick is to add some WiX specific extensions. This can be accomplished by adding a reference to the installer project, ‘Acme.Setup"’ in this example. The dialog box looks as follows and we want to add a file called ‘WixUtilExtension.dll’:

image

Adding this reference enables support for functionality that allows installing into the “Public Documents” folder.

The way we can use this is by adding a property reference with the Id ‘WIX_DIR_COMMON_DOCUMENTS’ as follows:

<Product Id="*" Name="Acme.Setup" Language="1033" Version="1.0.0.0" Manufacturer="Acme" UpgradeCode="YOURGUID-269e-4795-97d7-a392d75749fb">
    <Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />

    <MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
    <MediaTemplate />

   <PropertyRef Id="WIX_DIR_COMMON_DOCUMENTS"/>

   <Feature Id="ProductFeature" Title="Acme.Setup" Level="1">
        <ComponentGroupRef Id="ProductComponents" />
    </Feature>
</Product>

WiX will set the property ‘WIX_DIR_COMMON_DOCUMENTS’ to the correct path for the “Public Documents” folder when the installer is executed. We don’t have to worry about that.

Now this property has been referenced we can use it to specify where we would like to install our readme file. For that we first need to change the <Directory> tag content as follows:

<Fragment>
    <Directory Id="TARGETDIR" Name="SourceDir">
      <Directory Id="WIX_DIR_COMMON_DOCUMENTS">
         <Directory Id="ACMEDOCUFOLDER" Name="Acme Documentation">
         </Directory>
      </Directory>
      <Directory Id="ProgramFilesFolder">
            <Directory Id="INSTALLFOLDER" Name="Acme.Setup" />
        </Directory>
    </Directory>
</Fragment>
Note the additional directory tag with the Id ‘WIX_DIR_COMMON_DOCUMENTS’. I left the other directory tag in place. We don’t need it for the example in this blog post. For yuor actual installer you may want to use it to place binary files into a folder inside of “Program Files”.

The next change you have to make is adding a new component group that installs into the WIX_DIR_COMMON_DOCUMENTS folder. Here is the snippet that achieves this:

<Fragment>
    <ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
        <!-- TODO: Remove the comments around this Component element and the ComponentRef below in order to add resources to this installer. -->
        <!-- <Component Id="ProductComponent"> -->
            <!-- TODO: Insert files, registry keys, and other resources here. -->
      <!-- </Component> -->
   </ComponentGroup>
   <ComponentGroup Id="DocuComponents" Directory="ACMEDOCUFOLDER">
      <Component Id="ReadmeFile" Guid="{YOURGUID-994F-49C0-A7AC-53D1571A295A}">
         <File Id="readme.txt" Name="readme.txt" Source="..\Acme.Lib\readme.txt"></File>
      </Component>
   </ComponentGroup>
</Fragment>

Note that again I left the default component group untouched (Id=”ProductComponents”). Instead I created a new component group and gave it the id ‘DocuCompoents’. Then I moved the component ‘ReadmeFile’ from the former to the latter.

We are not quite finished yet. One item is missing. The installer does not yet know that it should include this new component / component group into the installer. So here is what we need to do to accomplish that:

<Product Id="*" Name="Acme.Setup" Language="1033" Version="1.0.0.0" Manufacturer="Acme" UpgradeCode="YOURGUID-269e-4795-97d7-a392d75749fb">
    <Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />

    <MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
    <MediaTemplate />

   <PropertyRef Id="WIX_DIR_COMMON_DOCUMENTS"/>

   <Feature Id="ProductFeature" Title="Acme.Setup" Level="1">
      <ComponentGroupRef Id="ProductComponents" />
      <ComponentGroupRef Id="DocuComponents" />
   </Feature>
</Product>

Once you build and execute the resulting installer then the result will look as follows:

image

And this screenshot demonstrates that we have accomplished what we wanted: We successfully installed a folder and a file into the “Public Documents” folder.

If you’d like the full source code of this solution please send an email to manfredmlange at gmail dot com.

Happy coding!

0 comments:

Post a Comment

All comments, questions and other feedback is much appreciated. Thank you!