[Source: http://geekswithblogs.net/EltonStoneman]
Snappy title. We have a project which contains lots of WCF Service projects, and we want to generate MSIs so we can deploy them to IIS. The gaps between the projects are minimal as they all use the same structure, so instead of having separate Setup or Wix files in each of the solutions, generating the WXS files on the fly was an option.
The installer steps we wanted were reasonably simple:
- install WCF artifacts to the chosen directory (.svc and web.config files)
- install WCF binaries to the chosen directory\bin
- install dependencies to the GAC
- create a virtual directory in IIS pointing to the install directory
The only UI we particularly needed was the choice of install directory, so the Wix for this fits into a fairly straightforward T4 template:
<#@ template language="C#" #>
<#@ output extension=".xml" #>
<#@ assembly name="System.dll" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Security.Cryptography" #>
<#@ import namespace="System.Text" #>
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:iis="http://schemas.microsoft.com/wix/IIsExtension">
<Product Id="<#= this.GetProductId() #>"
Name="<#= ServiceName #>"
Language="1033"
Version="<#= VersionNumber #>"
Manufacturer="Company"
UpgradeCode="<#= this.GetUpgradeCode() #>">
<Package InstallerVersion="200" Compressed="yes" />
<Media Id="1" Cabinet="<#= ServiceName #>.Install.cab" EmbedCab="yes" />
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder">
<Directory Id="CompanyFolder" Name="Company">
<Directory Id="INSTALLLOCATION" Name="<#= ServiceName #>">
<Component Id="Website" Guid="<#= this.GetNewGuid() #>">
<# foreach (string filePath in this.WcfArtifacts.Split(';'))
{
this.AddFile(filePath);
}#>
</Component>
<# int index = 0;
foreach (string assemblyPath in this.WcfDependencies.Split(';'))
{#>
<Component Id="GACComponent_<#= index++ #>" Guid="<#= this.GetNewGuid() #>" SharedDllRefCount="yes">
<# this.AddAssemblyToGAC(assemblyPath); #>
</Component>
<#}#>
<Directory Id="WebsiteBin" Name="bin">
<Component Id="WebsiteBin" Guid="<#= this.GetNewGuid() #>">
<# this.AddAssembly(this.WcfAssembly); #>
</Component>
</Directory>
</Directory>
</Directory>
</Directory>
<Component Id="VirtualDir" Guid="<#= this.GetNewGuid() #>">
<iis:WebVirtualDir Id="WcfVirtualDir" Alias="<#= ServiceName.TrimEnd(".Wcf".ToCharArray()) #>" Directory="INSTALLLOCATION" WebSite="DefaultWebSite">
<iis:WebApplication Id="WcfApplication" Name="<#= ServiceName #>" />
</iis:WebVirtualDir>
</Component>
</Directory>
<iis:WebSite Id="DefaultWebSite" Description="Default Web Site">
<iis:WebAddress Id="AllUnassigned" Port="80" />
</iis:WebSite>
<Feature Id="ProductFeature" Title="<#= ServiceName #>" Level="1">
<ComponentRef Id="Website" />
<ComponentRef Id="WebsiteBin" />
<# int i = 0;
foreach (string assemblyPath in this.WcfDependencies.Split(';'))
{#>
<ComponentRef Id="GACComponent_<#= i++ #>" />
<# } #>
<ComponentRef Id="VirtualDir" />
</Feature>
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLLOCATION"/>
<UIRef Id="WixUI_InstallDir" />
<WixVariable Id="WixUIDialogBmp" Value="$(WixPath)\SetupBackground.bmp" />
</Product>
</Wix>
<#+
private string ServiceName
{ get{ return "$(ProjectName).$(ProjectFilter)"; }}
private string VersionNumber
{ get{ return "$(Version)"; }}
private string WcfArtifacts
{ get{ return @"@(WcfArtifacts)"; }}
private string WcfAssembly
{ get{ return @"@(WcfAssembly)"; }}
private string WcfDependencies
{ get{ return @"@(WcfDependencies)"; }}
…
- the full template is here: WCF Service Wix template.
A couple of interesting things here – the Guids are generated from the name of the elements (see Generating Deterministic Guids), so multiple versions will have the same Guid and will operate as updates, provided the name of the service doesn't change; the tags for MSBuild properties in the template get resolved using the ExecuteT4Template MSBuild task; the GACd assemblies flag themselves as SharedDllRefCount so if you uninstall this package, the files will be left in the GAC if any other packages use them, but removed if this is the only package that uses them.
Another custom task is needed to resolve the dependencies of the WCF assembly (sample version here: MSBuild ResolveDependencies task), and the MSBuild snippet to run it together with the Wix commands looks like this:
<Target Name="ResolveWcfDependencies">
<!-- List the dependencies for the Wcf service:-->
<ResolveDependencies AssemblyList="@(WcfAssembly)"
Filter="$(CompanyName).$(ProductName)">
<Output TaskParameter="DependencyList" ItemName="WcfDependencies"/>
</ResolveDependencies>
</Target>
<Target Name="PublishWcf" DependsOnTargets=" ResolveWcfDependencies" Condition="'@(WcfAssembly)' != ''">
<!-- Create the wxs file: -->
<Message Text="Determined Wcf dependencies: @(WcfDependencies)"/>
<ExecuteT4Template ToolPath="$(MSBuildProjectDirectory)\$(BuildBinDir)"
TemplatePath="$(TemplateRoot)\Services.Wcf.wxs.tt"
OutputPath="$(ArtefactWixDir)\$(ProductName).$(ProjectName).Wcf.wxs">
<Output TaskParameter="TempFilePath" ItemName="T4TempFilePath"/>
</ExecuteT4Template>
<Message Text="T4 template resolved to: @(T4TempFilePath)"/>
<!-- Produce intermediate Wix obj:-->
<Exec Command='"$(WixPath)\candle" -out "$(ArtefactWixDir)\$(ProductName).$(ProjectName).Wcf.wixobj" "$(ArtefactWixDir)\$(ProductName).$(ProjectName).Wcf.wxs" -ext WixIIsExtension -ext WixUiExtension'/>
<!-- Produce MSI:-->
<Exec Command='"$(WixPath)\light" -out "$(PublishServiceDir)\$(ProductName).$(ProjectName).Wcf.msi" "$(ArtefactWixDir)\$(ProductName).$(ProjectName).Wcf.wixobj" -ext WixIIsExtension -ext WixUiExtension -cultures:en-us'/>
</Target>
- we store all the intermediate files (the T4 template with resolved property values, WXS and WIXOBJ files) to help with debugging.