Showing posts with label WIX. Show all posts
Showing posts with label WIX. Show all posts

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.

Tuesday, 2 August 2011

WIX 3 NETSH CustomAction

I've been writing a WIX 3 installer for a Windows Service with 2 self hosted WCF services. In order to connect to the WCF services, a Namespace reservation must be made using netsh.exe. I wanted to get the WIX installer to perform this operation during installation so decided to use a CustomAction to achieve this. As anybody who uses WIX will know, it is extremely powerful but sparsely documented and quiet frustrating to use. It took a while to get the CustomAction to work, so I thought I'd share the solution:

!-- Custom action to set WCF namespace reservation -->

<CustomAction Id="ListenerServiceAddReservation"
                  Directory="INSTALLLOCATION"
                  ExeCommand="[SystemFolder]netsh.exe http add urlacl url=http://+:8888/ServiceNamespace/TestService/ sddl=D:(A;;GX;;;WD)"
                  Return="asyncWait" />

<CustomAction Id="ListenerServiceDeleteReservation"
                  Directory="INSTALLLOCATION"
                  ExeCommand="[SystemFolder]netsh.exe http delete urlacl url=http://+:8888/ ServiceNamespace/TestService/"
                  Return="asyncWait" />
  
<InstallExecuteSequence>       
    <Custom Action="ListenerServiceDeleteReservation" Before="InstallFinalize">Installed</Custom>
    <Custom Action="ListenerServiceAddReservation" Before="InstallFinalize">NOT Installed</Custom>
</InstallExecuteSequence>