Friday, 28 June 2013

Using WiX Heat to Harvest Files for a WiX Installer

Overview

Manually adding files to a WiX installer can be fiddly and error prone. If you have a product with a large number of assemblies you will not want to type each file element manually and even if you have a small number of assemblies, there’s a chance you may add an extra one in the future and forget to add it.

Heat is the harvesting tool for WiX which can take care of this task by examining a directory and writing the contents to a fragment which can be incorporated into the WiX setup project.

Getting Heat Running

Heat can be run from the command line, but I like to be able to build the MSI in Visual Studio while I’m testing and in a single step during the build process. To do this some work needs to be done on the project properties in Visual Studio and manual modification to the wixproj file.

To start we need to tell Heat where to get the files from. To do this we need to define a pre-processor variable:


Be careful to set this for each build configuration you use.

Next we need to manually modify the wixproj file (I normally use notepad or notepad++). In an on-touched project, the last block with the Targets in is commented-out:

<!--
    To modify your build process, add your task inside one of the targets below and uncomment it.
    Other similar extension points exist, see Wix.targets.
    <Target Name="BeforeBuild">
    </Target>
    <Target Name="AfterBuild">
    </Target>
    -->
</Project>

We need to uncomment this block and add a HeatDirectory element containing all the settings for harvesting our files to the BeforeBuild target so that the files are ready for the build. This link to the documentation details what each attribute does: http://wix.sourceforge.net/manual-wix3/msbuild_task_reference_heatdirectory.htm

<Target Name="BeforeBuild">
    <HeatDirectory
NoLogo="$(HarvestDirectoryNoLogo)"
SuppressAllWarnings="$(HarvestDirectorySuppressAllWarnings)"
SuppressSpecificWarnings="$(HarvestDirectorySuppressSpecificWarnings)
"
ToolPath="$(WixToolPath)"
TreatWarningsAsErrors="$(HarvestDirectoryTreatWarningsAsErrors)"
TreatSpecificWarningsAsErrors="$(HarvestDirectoryTreatSpecificWarningsAsErrors)"
VerboseOutput="$(HarvestDirectoryVerboseOutput)"
AutogenerateGuids="$(HarvestDirectoryAutogenerateGuids)"
GenerateGuidsNow="$(HarvestDirectoryGenerateGuidsNow)"
OutputFile="Components.wxs"
SuppressFragments="$(HarvestDirectorySuppressFragments)"
SuppressUniqueIds="$(HarvestDirectorySuppressUniqueIds)"
Transforms="%(HarvestDirectory.Transforms)"
Directory="$(ProjectDir)Harvest"
ComponentGroupName="C_CommonAssemblies"
DirectoryRefId="INSTALLLOCATION"
KeepEmptyDirectories="false"
PreprocessorVariable="var.SourceDir"
SuppressCom="%(HarvestDirectory.SuppressCom)"
SuppressRootDirectory="true"
SuppressRegistry="%(HarvestDirectory.SuppressRegistry)">
    </HeatDirectory>
  </Target>
  <Target Name="AfterBuild">
  </Target>

I’ve highlighted the important bits which need filling in (for this scenatio):

  • Output File – This is the name of the file where the generated fragment goes
  • Directory –  This is the name of the directory where the file lives which will be written into the fragment
  • ComponentGroupName – Because heat generates a fragment with each file having it’s own component, it needs a component group name so they can all be associated and added to a feature.
  • DirectoryRefId – This is where in the package directory structure the files go.
  • PreprocessorVariable – This is where the variable declared in Visual Studio is linked.
  • SuppressRootDirectory – This is needed to stop heat creating a directory element in the fragment

<Fragment>
  <Directory Id="TARGETDIR" Name="SourceDir">
    <Directory Id="ProgramFilesFolder">
      <Directory Id="Manufacturer" Name="My Company">
        <Directory Id="INSTALLLOCATION" Name="My Product" />
        </Directory>
      </Directory>
    </Directory>
</Fragment>

Now, once the project is built, the output file should be created and can be included in the project. It should look like this (most chopped out):

<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
    <Fragment>
        <DirectoryRef Id="INSTALLLOCATION">
            <Component Id="cmp7BCDFB601A22996A1076B03EFE0C90F0" Guid="*">
                <File Id="fil9009CA87455BA8E51C93294588671D49" KeyPath="yes" Source="$(var.SourceDir)\MyApplication.exe" />
            </Component>
            <Component Id="cmpD21327D3291243C5AF8B30B1C2B0D068" Guid="*">
                <File Id="fil6A58F542D5E381F253E6964ADC0BE44B" KeyPath="yes" Source="$(var.SourceDir)\Common.dll" />
            </Component>           
            </Directory>
        </DirectoryRef>
    </Fragment>
    <Fragment>
        <ComponentGroup Id="C_CommonAssemblies">
            <ComponentRef Id="cmp7BCDFB601A22996A1076B03EFE0C90F0" />
            <ComponentRef Id="cmpD21327D3291243C5AF8B30B1C2B0D068" />
        </ComponentGroup>
    </Fragment>
</Wix>

Everything is wrapped in a DirectoryRef so it can be hooked into the main directory structure. Each file had a component and the components are linked together in a ComponentGroup element at the end.

Adding the ComonentGroup

Now that we have all the components for the files in a component group, the group needs adding to the required feature (with other components required):

<Feature Id="F_FullApplication" Title="Full Application" Level="1" Description="All Services" ConfigurableDirectory="INSTALLLOCATION">
    <ComponentGroupRef Id="C_CommonAssemblies" />
    <ComponentRef Id="C_OtherComponent" />
</Feature>

Now the installer should build with all the required files included.

Harvesting from a Website

The simplest thing to harvest from is an executable application as all the binaries are usually in a bin folder. Websites are different in that they need publishing so that all the required files and binaries are structured correctly in one place.

To do this manually in Visual Studio, go to Build -> Publish and setup a File System Publish:


Once published, harvest can be set to the published directory to collect all the files.

To do this from a command line for a build process, the following msbuild command can be performed on the web project I took out the stong naming bit for clarity):

msbuild WebProject.Web.csproj" /t:Clean;Build;_WPPCopyWebApplication /p:Configuration=Release;WebProjectOutputDir="WebProject.Website" /clp:summary

Harvesting from Multiple Sources to Single Fragment

If you need to harvest from multiple sources (we had 3 services which we wanted to run from one folder with mostly common assemblies) you can simply add a script before the harvest step to copy the sources into one directory. This can’t be done in a pre-build event unfortunately because this happens after the Target BeforeBuild, so we can put in an exec task like this:

<Target Name="BeforeBuild">
    <Exec Command="
REM Copy all service build folders into one for harvesting

if exist $(ProjectDir)Harvest rd $(ProjectDir)Harvest /s /q

md $(ProjectDir)Harvest

xcopy /s /i /q /y /c /d $(SolutionDir)App1\bin\$(ConfigurationName) $(ProjectDir)Harvest\

xcopy /s /i /q /y /c /d $(SolutionDir)App2\bin\$(ConfigurationName) $(ProjectDir)Harvest\

xcopy /s /i /q /y /c /d $(SolutionDir)IridiumManagerService\bin\$(ConfigurationName) $(ProjectDir)Harvest\">
    </Exec>
    <HeatDirectory [as before]
    </HeatDirectory>
  </Target>

The files can then be harvested from the Harvest folder under the setup project directory.

Removing exe files from the harvested fragment

If you need to manually write a component for the executables which you will if you want to add ProgId and RegistryValue elements for file associations etc, or you need to setup a service component, it is necessary to strip out the exe files from the fragment heat generates.

To do this a transform can be added to the HeatDirectory block to transform the output. Add a file to the project called Transorm.xsl and enter that name into the Transform attribute. The file should look like this:

<?xml version="1.0" ?>
<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:wix="http://schemas.microsoft.com/wix/2006/wi">

  <!-- Copy all attributes and elements to the output. -->
  <xsl:template match="@*|*">
    <xsl:copy>
      <xsl:apply-templates select="@*" />
      <xsl:apply-templates select="*" />
    </xsl:copy>
  </xsl:template>

  <xsl:output method="xml" indent="yes" />

  <xsl:key name="exe-search" match="wix:Component[contains(wix:File/@Source, '.exe')]" use="@Id" />
  <xsl:template match="wix:Component[key('exe-search', @Id)]" />
  <xsl:template match="wix:ComponentRef[key('exe-search', @Id)]" />
</xsl:stylesheet>

Conclusion


I hope all or some of this is helpful, it took a long time to work out a lot of this stuff from reading various sources.

15 comments:

  1. var.SourceDir is unresolvable in the generated file. I used



    to define it in the Product.wxs, but it's not visible in the generated file apparently.

    ReplyDelete
  2. "PreprocessorVariable – This is where the variable declared in Visual Studio is linked."

    you need to define it in the wix project properties under build tab

    ReplyDelete
  3. You give an example of how to harvest every file from a directory, and every file from a website.

    Is there an article for how to harvest the outputs of other projects in the same solution?

    Thanks.
    -Jesse

    ReplyDelete
  4. I don't tend to use project refs with heat as it runs before build, so the dependencies aren't available at this time

    ReplyDelete
    Replies
    1. Sad.
      All the projects that the WiX project has references to have been built already, so all their dependencies really ought to be knowable.
      And they were knowable back in the pre-WiX VS-built-in-Setup days.

      Thanks.
      -Jesse

      Delete
    2. You can use project ref variables if you know they're built, but i think you can get file locking issues between heat and vs.

      Delete
    3. The problem isn't that project output references can't be put in a WiX file. And the project dependencies keep the file locking from being an issue. The problem is that Heat is barely able to go one deep. It doesn't chase dependencies to other projects in the same solution, let alone dependencies to dll/assemblies outside the solution. What used to be trivial in the pre-WiX days is now cumbersome. All necessary information is available in VS, but apparently not in WiX. I think it will require an extension to VS to do the harvesting from inside VS where the information lives.
      -Jesse

      Delete
  5. Thank you. This article was really useful. It saved me a lot of time.

    - Jacob (Io-Interactive)

    ReplyDelete
  6. Thank you so much.

    ReplyDelete
  7. Expected "$(EnableProjectHarvesting)" to evaluate to a boolean instead of "True ", in condition " $(EnableProjectHarvesting) and ('$(OutputType)' == 'Package' or '$(OutputType)' == 'PatchCreation' or '$(OutputType)' == 'Module') "

    ReplyDelete
  8. Thank you very very much. This saved me a ton of time!!

    ReplyDelete
  9. Thank you. Now I understand how to copy multiple files using heat.

    ReplyDelete
  10. Thanks. It really helps me a lot.

    ReplyDelete
  11. Thank you very much for sharing this hard earned information. This article was extremely helpful!

    ReplyDelete