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.
var.SourceDir is unresolvable in the generated file. I used
ReplyDeleteto define it in the Product.wxs, but it's not visible in the generated file apparently.
"PreprocessorVariable – This is where the variable declared in Visual Studio is linked."
ReplyDeleteyou need to define it in the wix project properties under build tab
Like in the picture
ReplyDeleteYou give an example of how to harvest every file from a directory, and every file from a website.
ReplyDeleteIs there an article for how to harvest the outputs of other projects in the same solution?
Thanks.
-Jesse
I don't tend to use project refs with heat as it runs before build, so the dependencies aren't available at this time
ReplyDeleteSad.
DeleteAll 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
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.
DeleteThe 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.
Delete-Jesse
Thank you. This article was really useful. It saved me a lot of time.
ReplyDelete- Jacob (Io-Interactive)
Thank you so much.
ReplyDeleteExpected "$(EnableProjectHarvesting)" to evaluate to a boolean instead of "True ", in condition " $(EnableProjectHarvesting) and ('$(OutputType)' == 'Package' or '$(OutputType)' == 'PatchCreation' or '$(OutputType)' == 'Module') "
ReplyDeleteThank you very very much. This saved me a ton of time!!
ReplyDeleteThank you. Now I understand how to copy multiple files using heat.
ReplyDeleteThanks. It really helps me a lot.
ReplyDeleteThank you very much for sharing this hard earned information. This article was extremely helpful!
ReplyDelete