天天看點

WiX 3 tutorialUsing Project References and Variables



WiX 3 Tutorial 1: Solution/Project structure and Dev resources

This is the first post about building a WiX 3 installer from zero. The reason I’ve decided to write this WiX series is that the good sources are quite hard to find and examples even harder.

The finished installer (at the end of the series) will be localized in 2 languages, have localized EULA check, product key validation via a custom action, updating (and killing the running app when updating) and wrapping both localized MSI’s into a bootstrapper that will enable you to choose the installation language and install prerequisites like .Net framework 3.5. Those are the general features and we’ll well dive into each feature in a separate post.

We’ll be using WiX 3.0.5419.0 in Visual Studio 2008. You can download WiX at theSourceForge Wix Site. I’m expecting that you’re familiar with basic WiX operations and how to make the WiX project.

To help you with your WiX development be sure to use the ORCA tool (download viaWin SDK or directly fromhere) for viewing/editing MSI tables and defaultMSI logging capabilities like “msiexec /i SuperForm.Installer.msi /l*v SuperFormLog.txt” which will write a very detailed log of what it’s doing at each step.

The application we’ll be installing is called SuperForm which solves all your label’s text color needs by using the awesome action of pressing a button.

The first thing we have to do is to create a new System Environment variable SuperFormFilesDir containing the path to the directory where you keep the files you’ll install on the users computer. We’ll be using this variable in the automated building of wxs fragment file that holds the correct directory/files structure. This is my preference in environments with multiple developers. It is highly unlikely that all developers have the same directory structure. If you work alone you can also take the “preprocessor variables” route described below in Project Properties –> Build. I’ll be using the System Environment variable approach.

Solution/Project structure

WiX 3 tutorialUsing Project References and Variables

We have three projects of which the most important for us is the WiX project. Logically the project is divided into 7 folders and 1 main file.

  1. CustomActions: stores WiX fragments that define different custom actions (CA’s). You can have 1 fragment per CA or all CA’s under 1 fragment. I prefer fragment per CA.
  2. CustomDialogs: stores WiX fragments that define our custom built dialogs. there are 2 we’ll have: The overwritten existing EULA dialog and fully custom product key check dialog.
  3. Fragments: stores WiX fragments that are either auto generated by Heat.exe directory harvesting or manually built like inserting some values into the registry.
  4. Includes: stores WiX include files that hold pre processor variables. Wxi files must be included at the top of each wxs file you’re using variables in.
  5. Lang: stores localization stuff
    1. de-de: German installer. Stores German EULA, custom localization file and official German WiX translations.
    2. en-us: US English installer. Stores English EULA and custom localization file.
  6. Packages: output for the actual MSI’s. This replaces the bin folder. 
    1. de-de: stores the German MSI installer.
    2. en-us: stores the English MSI installer.
  7. Resources: stores different resources like icons, jpg’s, etc… used in the installer.
  8. SuperForm.wxs: master installer files where all the magic happens.

We also need to include some references:

  1. SuperForm.CustomAction: needed to run our custom actions stored in the SuperForm.CustomAction.dll project
  2. WixNetFxExtension: needed for conditional check id .Net 3.5 is installed and abort if not.
  3. WixUIExtension: needed to include and modify any UI elements. 
  4. WixUtilExtension: needed to run CloseApplication built in custom action.

All non WiX files (others than wsx, wxi, wxl) don’t really need to be included in the project but i like to include them for clarity. This way we see exactly what is in the installer from the project itself.

SuperForm.Install WiX project properties:

  1. Installer: No changes
  2. Build:
    1. Change “Output Path” to “Packages\”. This is needed to respect the project structure. You could use the default ($Configuration)\bin but I prefer not to. The ($Configuration) part is only useful if you’re building both 32 and 64 bit installers.
    2. If you don’t like adding a new System Environment variable you can do this instead. To “Define preprocessor variables” add the SuperFormFilesDir=pathToTheFolderWhereTheFilesToBeInstalledAre;”. Variables are delimited by semicolons (;).
  3. Build Events:
    1. To “Pre-build event” add "$(WIX)bin\heat.exe" dir "$(SuperFormFilesDir)" -cg SuperFormFiles -gg -scom -sreg -sfrag -srd -dr INSTALLLOCATION -var env.SuperFormFilesDir -out "$(ProjectDir)Fragments\FilesFragment.wxs". Heat.exe makes a wxs file with proper directory/file structure from the specified directory. We’ll take a detailed look into this in a future post.
    2. You can skip this for now but remember to add it later! To “Post-build event” add “$(SolutionDir)BootstrapperBuild.bat $(TargetDir) $(TargetFileName) $(SolutionDir)”. This will be used at the end to build the bootstrapper.
  4. Paths: No changes
  5. Tool settings: No changes

Resources for WiX development

  1. WiX Documentation – official docs, what else.
  2. Rob Mensching – creator and main contributor of WiX.
  3. WiX tutorial by Gábor DEÁK JAHN – this is an excellent tutorial that covers all aspects of WiX!
  4. From MSI to WiX by Alex Shevchuk – a Technet series on how to build an installer with WiX.
  5. WiX Mindcapers wiki – great wiki with lots of great stuff
  6. Joy of Setup by Bob Arnson – a setup blog with lots of good info.
  7. WiX tricks and best practices – StackOverflow wiki with some more links to best practices with WiX.

WiX 3 Tutorial 2: Understanding main WXS and WXI file

In the previous post we’ve taken a look at the WiX solution/project structure and project properties. We’re still playing with our super SuperForm application and today we’ll take a look at the general parts of the main wxs file, SuperForm.wxs, and the wxi include file. For wxs file we’ll just go over the general description of what each part does in the code comments. The more detailed descriptions will be in future posts about features themselves.

WXI include file

Include files are exactly what their name implies. To include a wxi file into the wxs file you have to put the wxi at the beginning ofeach .wxs file you wish to include it in. If you’ve ever worked with C++ you can think of the include files as .h files. For example if you include SuperFormVariables.wxi into the SuperForm.wxs, the variables in the wxi won’t be seen in FilesFragment.wxs or RegistryFragment.wxs. You’d have to include it manually into those two wxs files too.

For preprocessor variable $(var.VariableName) to be seen by every file in the project you have to include them in the WiX project properties->Build->“Define preprocessor variables” textbox.

This is why I’ve chosen not to go this route because in multi developer teams not everyone has the same directory structure and having a single variable would mean each developer would have to checkout the wixproj file to edit the variable. This is pretty much unacceptable by my standards. This is why we’ve added a System Environment variable named SuperFormFilesDir as is shown in the previousWix Tutorial post. Because the FilesFragment.wxs is autogenerated on every project build we don’t want to change it manually each time by adding the include wxi at the beginning of the file. This way we couldn’t recreate it in each pre-build event.

<?xml version="1.0" encoding="utf-8"?>
<Include>
  <!--
  Versioning. These have to be changed for upgrades.
  It's not enough to just include newer files.
  -->
  <?define MajorVersion="1" ?>
  <?define MinorVersion="0" ?>
  <?define BuildVersion="0" ?>
  <!-- Revision is NOT used by WiX in the upgrade procedure -->
  <?define Revision="0" ?>
  <!-- Full version number to display -->
  <?define VersionNumber="$(var.MajorVersion).$(var.MinorVersion).$(var.BuildVersion).$(var.Revision)" ?>
  <!--
  Upgrade code HAS to be the same for all updates.
  Once you've chosen it don't change it.
  -->
  <?define UpgradeCode="YOUR-GUID-HERE" ?>
  <!--
  Path to the resources directory. resources don't really need to be included
  in the project structure but I like to include them for for clarity 
  -->
  <?define ResourcesDir="$(var.ProjectDir)\Resources" ?>  
  <!--
  The name of your application exe file. This will be used to kill the process when updating
  and creating the desktop shortcut
  -->
  <?define ExeProcessName="SuperForm.MainApp.exe" ?>
</Include>
           

For now there’s no way to tell WiX in Visual Studio to have a wxi include file available to the whole project, so you have to include it in each file separately.

Only variables set in “Define preprocessor variables” or System Environment variables are accessible to the whole project for now.

The main WXS file: SuperForm.wxs

We’ll only take a look at the general structure of the main SuperForm.wxs and not its the details. We’ll cover the details in future posts. The code comments should provide plenty info about what each part does in general.

Basically there are 5 major parts. The update part, the conditions and actions part, the UI install sequence, the directory structure and the features we want to include.

<?xml version="1.0" encoding="UTF-8"?>
<!-- Add xmlns:util namespace definition to be able to use stuff from WixUtilExtension dll-->
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
  <!-- This is how we include wxi files -->
  <?include $(sys.CURRENTDIR)Includes\SuperFormVariables.wxi ?>
  <!-- 
  Id="*" is to enable upgrading. * means that the product ID will be autogenerated on each build. 
  Name is made of localized product name and version number.
  -->
  <Product Id="*" Name="!(loc.ProductName) $(var.VersionNumber)" Language="!(loc.LANG)" Version="$(var.VersionNumber)" Manufacturer="!(loc.ManufacturerName)" UpgradeCode="$(var.UpgradeCode)">
    <!-- Define the minimum supported installer version (3.0) and that the install should be done for the whole machine not just the current user -->
    <Package InstallerVersion="300" Compressed="yes" InstallScope="perMachine"/>
    <Media Id="1" Cabinet="media1.cab" EmbedCab="yes" />    
    <!-- Upgrade settings. This will be explained in more detail in a future post -->
    <Upgrade Id="$(var.UpgradeCode)">
      <UpgradeVersion OnlyDetect="yes" Minimum="$(var.VersionNumber)" IncludeMinimum="no" Property="NEWER_VERSION_FOUND" />
      <UpgradeVersion Minimum="0.0.0.0" IncludeMinimum="yes" Maximum="$(var.VersionNumber)" IncludeMaximum="no" Property="OLDER_VERSION_FOUND" />
    </Upgrade>    
    <!-- Reference the global NETFRAMEWORK35 property to check if it exists -->
    <PropertyRef Id="NETFRAMEWORK35"/>    
    <!-- 
    Startup conditions that checks if .Net Framework 3.5 is installed or if 
    we're running the OS higher than Windows XP SP2.
    If not the installation is aborted.
    By doing the (Installed OR ...) property means that this condition will only 
    be evaluated if the app is being installed and not on uninstall or changing
    -->
    <Condition Message="!(loc.DotNetFrameworkNeeded)">
      <![CDATA[Installed OR NETFRAMEWORK35]]>
    </Condition>
    <Condition Message="!(loc.AppNotSupported)">
      <![CDATA[Installed OR ((VersionNT >= 501 AND ServicePackLevel >= 2) OR (VersionNT >= 502))]]>
    </Condition>
    <!-- 
    This custom action in the InstallExecuteSequence is needed to 
    stop silent install (passing /qb to msiexec) from going around it. 
    -->
    <CustomAction Id="NewerVersionFound" Error="!(loc.SuperFormNewerVersionInstalled)" />
    <InstallExecuteSequence>
      <!-- Check for newer versions with FindRelatedProducts and execute the custom action after it -->
      <Custom Action="NewerVersionFound" After="FindRelatedProducts">
        <![CDATA[NEWER_VERSION_FOUND]]>
      </Custom>
      <!-- Remove the previous versions of the product -->
      <RemoveExistingProducts After="InstallInitialize"/>
      <!-- WixCloseApplications is a built in custom action that uses util:CloseApplication below -->
      <Custom Action="WixCloseApplications" Before="InstallInitialize" />
    </InstallExecuteSequence>
    <!-- This will ask the user to close the SuperForm app if it's running while upgrading -->
    <util:CloseApplication Id="CloseSuperForm" CloseMessage="no" Description="!(loc.MustCloseSuperForm)" 
                           ElevatedCloseMessage="no" RebootPrompt="no" Target="$(var.ExeProcessName)" />
    <!-- Use the built in WixUI_InstallDir GUI -->
    <UIRef Id="WixUI_InstallDir" />
    <UI>
      <!-- These dialog references are needed for CloseApplication above to work correctly -->
      <DialogRef Id="FilesInUse" />
      <DialogRef Id="MsiRMFilesInUse" />      
      <!-- Here we'll add the GUI logic for installation and updating in a future post-->      
    </UI>
    <!-- Set the icon to show next to the program name in Add/Remove programs -->
    <Icon Id="SuperFormIcon.ico" SourceFile="$(var.ResourcesDir)\Exclam.ico" />
    <Property Id="ARPPRODUCTICON" Value="SuperFormIcon.ico" />
    <!-- Installer UI custom pictures. File names are made up. Add path to your pics. –>
    <!--
    <WixVariable Id="WixUIDialogBmp" Value="MyAppLogo.jpg" />
    <WixVariable Id="WixUIBannerBmp" Value="installBanner.jpg" />
    -->
    <!-- the default directory structure -->
    <Directory Id="TARGETDIR" Name="SourceDir">
      <Directory Id="ProgramFilesFolder">
        <Directory Id="INSTALLLOCATION" Name="!(loc.ProductName)" />
      </Directory>
    </Directory>
    <!-- 
    Set the default install location to the value of 
    INSTALLLOCATION (usually c:\Program Files\YourProductName) 
    -->
    <Property Id="WIXUI_INSTALLDIR" Value="INSTALLLOCATION" />
    <!-- Set the components defined in our fragment files that will be used for our feature  -->
    <Feature Id="SuperFormFeature" Title="!(loc.ProductName)" Level="1">
      <ComponentGroupRef Id="SuperFormFiles" />
      <ComponentRef Id="cmpVersionInRegistry" />
      <ComponentRef Id="cmpIsThisUpdateInRegistry" />

    </Feature>
  </Product>
</Wix>
           

For more info on what certain attributes mean you should look into the WiX Documentation.

WiX 3 Tutorial 3: Generating file/directory fragments with Heat.exe

In previous posts I’ve shown you our SuperForm test application solution structure and how themain wxs and wxi include file look like. In this post I’ll show you how to automate inclusion of files to install into your build process. For our SuperForm application we have a single exe to install. But in the real world we have 10s or 100s of different files from dll’s to resource files like pictures. It all depends on what kind of application you’re building. Writing a directory structure for so many files by hand is out of the question. What we need is an automated way to create this structure. Enter Heat.exe.

Heat is a command line utility to harvest a file, directory, Visual Studio project, IIS website or performance counters. You might ask what harvesting means? Harvesting is converting a source (file, directory, …) into a component structure saved in a WiX fragment (a wxs) file.

There are 2 options you can use:

  1. Create a static wxs fragment with Heat and include it in your project. The pro of this is that you can add or remove components by hand. The con is that you have to do the pro part by hand. Automation always beats manual labor.
  2. Run heat command line utility in a pre-build event of your WiX project. I prefer this way. By always recreating the whole fragment you don’t have to worry about missing any new files you add. The con of this is that you’ll include files that you otherwise might not want to.

There is no perfect solution so pick one and deal with it. I prefer using the second way. A neat way of overcoming the con of the second option is to have a post-build event on your main application project (SuperForm.MainApp in our case) to copy the files needed to be installed in a special location and have the Heat.exe read them from there. I haven’t set this up for this tutorial and I’m simply including all files from the default SuperForm.MainApp \bin directory.

Remember how we created a System Environment variable called SuperFormFilesDir? This is where we’ll use it for the first time. The command line text that you have to put into the pre-build event of your WiX project looks like this:

"$(WIX)bin\heat.exe" dir "$(SuperFormFilesDir)" -cg SuperFormFiles -gg -scom -sreg -sfrag -srd -dr INSTALLLOCATION -var env.SuperFormFilesDir -out "$(ProjectDir)Fragments\FilesFragment.wxs"

After you install WiX you’ll get the WIX environment variable. In the pre/post-build events environment variables are referenced like this: $(WIX). By using this you don’t have to think about the installation path of the WiX. Remember: for 32 bit applications Program files folder is named differently between 32 and 64 bit systems. $(ProjectDir) is obviously the path to your project and is a Visual Studio built in variable.

You can view all Heat.exe options by running it without parameters but I’ll explain some that stick out the most.

  1. dir "$(SuperFormFilesDir)": tell Heat to harvest the whole directory at the set location. That is the location we’ve set in our System Environment variable.
  2. –cg SuperFormFiles: the name of the Component group that will be created. This name is included in out Feature tag as is seen inthe previous post.
  3. -dr INSTALLLOCATION: the directory reference this fragment will fall under. You can see the top level directory structure inthe previous post.
  4. -var env.SuperFormFilesDir: the name of the variable that will replace the SourceDir text that would otherwise appear in the fragment file.
  5. -out "$(ProjectDir)Fragments\FilesFragment.wxs": the full path and name under which the fragment file will be saved.

If you have source control you have to include the FilesFragment.wxs into your project but remove its source control binding. The auto generated FilesFragment.wxs for our test app looks like this:

<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
    <Fragment>
        <ComponentGroup Id="SuperFormFiles">
            <ComponentRef Id="cmp5BB40DB822CAA7C5295227894A07502E" />
            <ComponentRef Id="cmpCFD331F5E0E471FC42A1334A1098E144" />
            <ComponentRef Id="cmp4614DD03D8974B7C1FC39E7B82F19574" />
            <ComponentRef Id="cmpDF166522884E2454382277128BD866EC" />
        </ComponentGroup>
    </Fragment>
    <Fragment>
        <DirectoryRef Id="INSTALLLOCATION">
            <Component Id="cmp5BB40DB822CAA7C5295227894A07502E" Guid="{117E3352-2F0C-4E19-AD96-03D354751B8D}">
                <File Id="filDCA561ABF8964292B6BC0D0726E8EFAD" KeyPath="yes" Source="$(env.SuperFormFilesDir)\SuperForm.MainApp.exe" />
            </Component>
            <Component Id="cmpCFD331F5E0E471FC42A1334A1098E144" Guid="{369A2347-97DD-45CA-A4D1-62BB706EA329}">
                <File Id="filA9BE65B2AB60F3CE41105364EDE33D27" KeyPath="yes" Source="$(env.SuperFormFilesDir)\SuperForm.MainApp.pdb" />
            </Component>
            <Component Id="cmp4614DD03D8974B7C1FC39E7B82F19574" Guid="{3443EBE2-168F-4380-BC41-26D71A0DB1C7}">
                <File Id="fil5102E75B91F3DAFA6F70DA57F4C126ED" KeyPath="yes" Source="$(env.SuperFormFilesDir)\SuperForm.MainApp.vshost.exe" />
            </Component>
            <Component Id="cmpDF166522884E2454382277128BD866EC" Guid="{0C0F3D18-56EB-41FE-B0BD-FD2C131572DB}">
                <File Id="filF7CA5083B4997E1DEC435554423E675C" KeyPath="yes" Source="$(env.SuperFormFilesDir)\SuperForm.MainApp.vshost.exe.manifest" />
            </Component>
        </DirectoryRef>
    </Fragment>
</Wix>
           

The $(env.SuperFormFilesDir) will be replaced at build time with the directory where the files to be installed are located. There is nothing too complicated about this. In the end it turns out that this sort of automation is great!

There are a few other ways that Heat.exe can compose the wxs file but this is the one I prefer. It just seems the clearest. Play with its options to see what can it do. It’s one awesome little tool.

WiX 3 tutorial by Mladen Prajdić navigation

  • WiX 3 Tutorial: Solution/Project structure and Dev resources
  • WiX 3 Tutorial: Understanding main wxs and wxi file
  • WiX 3 Tutorial: Generating file/directory fragments with Heat.exe 
  • WiX 3 Tutorial: Custom EULA License and MSI localization

Using Project References and Variables

The WiX project supports adding project references to other projects such as VB and C#. This ensures that build order dependencies are defined correctly within the solution. In addition, it generates a set of WiX preprocessor variables that can be referenced in WiX source files and preprocessor definitions which are passed to the compiler at build time.

To add a project reference to a WiX project:

  1. Right-click on the References node of the project in the Solution Explorer and choose Add Reference....
  2. In the Add Reference dialog, click on the Projects tab.
  3. Select the desired project(s) and click the Add button, and then press OK to dismiss the dialog.

Supported Project Reference Variables

Once a project reference is added, a list of project variables becomes avaliable to be referenced in the WiX source code. Project reference variables are useful when you do not want to have hard-coded values. For example, the $(var.MyProject.ProjectName) variable will query the correct project name at build time even if I change the name of the referenced project after the reference is added.

The following demonstrates how to use project reference variables in WiX source code:

<File Id="MyExecutable" Name="$(var.MyProject.TargetFileName)" Source="$(var.MyProject.TargetPath)" DiskId="1" />
           

The WiX project supports the following project reference variables:

Variable name Example usage Example value
var.ProjectName.Configuration $(var.MyProject.Configuration) Debug or Release
var.ProjectName.FullConfiguration $(var.MyProject.FullConfiguration) Debug|AnyCPU
var.ProjectName.Platform $(var.MyProject.Platform) AnyCPU, Win32, x64 or ia64
var.ProjectName.ProjectDir $(var.MyProject.ProjectDir) C:\users\myusername\Documents\Visual Studio 2010\Projects\MyProject\
var.ProjectName.ProjectExt $(var.MyProject.ProjectExt) .csproj
var.ProjectName.ProjectFileName $(var.MyProject.ProjectFileName) MyProject.csproj
var.ProjectName.ProjectName $(var.MyProject.ProjectName) MyProject
var.ProjectName.ProjectPath $(var.MyProject.ProjectPath) C:\users\myusername\Documents\Visual Studio 2010\Projects\MyProject\MyApp.csproj
var.ProjectName.TargetDir $(var.MyProject.TargetDir) C:\users\myusername\Documents\Visual Studio 2010\Projects\MyProject\bin\Debug\
var.ProjectName.TargetExt $(var.MyProject.TargetExt) .exe
var.ProjectName.TargetFileName $(var.MyProject.TargetFileName) MyProject.exe
var.ProjectName.TargetName $(var.MyProject.TargetName) MyProject
var.ProjectName.TargetPath $(var.MyProject.TargetPath) C:\users\myusername\Documents\Visual Studio 2010\Projects\MyProject\bin\Debug\MyProject.exe
var.ProjectName.Culture.TargetPath $(var.MyProject.en-US.TargetPath) C:\users\myusername\Documents\Visual Studio 2010\Projects\MyProject\bin\Debug\en-US\MyProject.msm
var.SolutionDir $(var.SolutionDir) C:\users\myusername\Documents\Visual Studio 2010\Projects\MySolution\
var.SolutionExt $(var.SolutionExt) .sln
var.SolutionFileName $(var.SolutionFileName) MySolution.sln
var.SolutionName $(var.SolutionName) MySolution
var.SolutionPath $(var.SolutionPath) C:\users\myusername\Documents\Visual Studio 2010\Projects\MySolution\MySolution.sln

Note: var.ProjectName.Culture.TargetPath is only available for projects that have multiple localized outputs (e.g. MSMs).