Thursday, June 26, 2008

How to use MSBuild and Wix to create msi package and then test scripts using NUnit?

Problem

Last few clients been requesting us to create MSBuild and Wix scripts to be incorporated into their daily builds as part of Continous Integration (CI) practice. I been struggling to find good documentation on how Wix work with MSBuild and how to create reusable Wix scripts.

1) How to integrate Wix scripts to MSBuild?
2) How to create generic Wix scripts that can be used in the future?
3) What is bootstrapper? How to create bootstrapper?
4) How to write unit test to test MSBuild and Wix 3.0 scripts?
5) What is t4 templates? How to write t4 templates to help with Wix?


References used to research and learn

http://blogs.technet.com/alexshev/pages/from-msi-to-wix.aspx
http://www.olegsych.com/2007/12/text-template-transformation-toolkit/
http://www.sedodream.com/
http://wixedit.sourceforge.net/
http://community.sharpdevelop.net/blogs/mattward/archive/2006/09/17/WixIntegration.aspx
http://msdn.microsoft.com/en-us/library/ms164294.aspx
http://msdn.microsoft.com/en-us/library/aa302186.aspx
http://geekswithblogs.net/vagmi.mudumbai/archive/2004/11/23/15669.aspx
http://blogs.msdn.com/robmen/archive/2003/10/18/56497.aspx

Assumption


1) The reader is somewhat familiar with MSBuild
1) The reader is somewhat familiar with Wix


About the project


1) MSBuild script generic.wixproj will be responsible executing Wix scripts which will create MSI packages.
2) MSBuild properties such as InstallVersion, UpgradeCode and any custom properties can be passed to Wix script as preprocessor. The custom properties passed to Wix scripts will be used like $(var.MyValueSentFromMSBuild).
3) Wix scripts are broken into fragments thanks to nice and short blog of best practice tips from http://geekswithblogs.net/vagmi.mudumbai/archive/2004/11/23/15669.aspx

Setup.wxs uses pre-built WixUI_InstallDir. Directory and Features in Setup.wxs only act as place holders and will be provided by external Wix fragment file called Files.wxs.


Download GenericWixDemo.zip


Requirements


.Net 3.5
VS2008
NUnit
Wix 3.0 toolset


Step by step instruction

1) First unzip the demo to GenericWixDemo directory
2) download latest Wix 3.0 toolset from Wix 3.0 toolset and unzip to GenericWixDemo



generic.wixproj

<Project ToolsVersion="3.5" DefaultTargets="BuildWix" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
 
   <!--
   ==================================================================================================
      These are REQUIRED by Setup.wxs WIX script
      [IN]
         "OutputName"
         "OutputType"
         "WixToolPath"
         "MsiOutputPath"
         "LicenseFile"
         "WixSetupFile"
         "WixUIDialogBmp"
         "WixUIBannerBmp"
         "ApplicationName"         
         "InstallVersion"
         "CompanyName"         
         "CompanyUID"
         "WixDirectoryFregment"
         "UpgradeCode"
         "ProductId"
         "Compressed"
         "Description"
   ==================================================================================================
   -->
   <PropertyGroup>
      <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
      <Cultures>en-US</Cultures>
      <DefineSolutionProperties>False</DefineSolutionProperties>
   </PropertyGroup>
 
   <PropertyGroup>
      <!-- 
      Required by Wix 
      -->
      <WixToolPath Condition="!HasTrailingSlash('$(WixToolPath)') ">$(WixToolPath)\</WixToolPath>
      <WixTasksPath>$(WixToolPath)WixTasks.dll</WixTasksPath>
      <WixTargetsPath>$(WixToolPath)Wix.targets</WixTargetsPath>
      <OutputPath Condition="'$(MsiOutputPath)' != ''">$(MsiOutputPath)</OutputPath>
      <OutputPath Condition="!HasTrailingSlash('$(OutputPath)') ">$(OutputPath)\</OutputPath>
 
      <!--   Required by Wix Preprocessing variables; 
            basically passing external masbuild property to Wix script.
            i.e. to use InstallVersion in Wix do $(var.INSTALLVERSION) -->
      <WixVariables>WixUILicenseRtf=$(LicenseFile);WixUIDialogBmp=$(WixUIDialogBmp);WixUIBannerBmp=$(WixUIBannerBmp)</WixVariables>
      <DefineConstants>
         INSTALLVERSION=$(InstallVersion)
         ;APPLICATIONNAME=$(ApplicationName)
         ;COMPANYNAME=$(CompanyName)
         ;COMPANYUID=$(CompanyUID)
         ;UPGRADECODE=$(UpgradeCode)
         ;PRODUCTID=$(ProductId)
         ;COMPRESSED=$(Compressed)
         ;DESCRIPTION=$(Description)
      </DefineConstants>
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(Configuration)' == 'Debug'">
      <DebugSymbols Condition="'$(DebugSymbols)' == ''" >True</DebugSymbols>
      <DebugType Condition="'$(DebugType)' == ''" >Full</DebugType>
      <Optimize Condition="'$(Optimize)' == ''" >False</Optimize>
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(DebugSymbols)' == 'Release' ">
      <DebugSymbols Condition="'$(DebugSymbols)' == ''" >False</DebugSymbols>
      <DebugType Condition=" '$(DebugType)' == ''" >None</DebugType>
      <Optimize Condition=" '$(Optimize)' == ''" >True</Optimize>
   </PropertyGroup>
 
   <Import Project="$(WixTargetsPath)" />
 
   <ItemGroup>
      <WixExtension Include="$(WixToolPath)WixNetFxExtension.dll" />
      <WixExtension Include="$(WixToolPath)WixUIExtension.dll" />
      <Compile Include="$(WixDirectoryFregment)" />
      <Compile Include="$(WixSetupFile)" />
   </ItemGroup>
 
 
   <!-- Create .Net 3.5 boostrapper to be used by Wix to install .net 3.5 if the user computer does not exist -->
   <ItemGroup>
      <BootstrapperFile Include="Microsoft.Net.Framework.3.5">
         <ProductName>Microsoft .NET Framework 3.5</ProductName>
      </BootstrapperFile>
      <BootstrapperFile Include="Microsoft.Windows.Installer.3.1">
         <ProductName>Windows Installer 3.1</ProductName>
      </BootstrapperFile>
   </ItemGroup>
 
   <Target Name="BuildWix" DependsOnTargets="ValidateGenericWix;Build">
      <GenerateBootstrapper
         ApplicationFile="$(OutputName).msi"
         ApplicationName="$(ApplicationName)"
         BootstrapperItems="@(BootstrapperFile)"
         OutputPath="$(OutputPath)"
         ComponentsLocation="HomeSite"
         Culture="en"
         CopyComponents="false"
         SupportUrl="http://www.microsoft.com/downloads/details.aspx?familyid=333325FD-AE52-4E35-B531-508D977D32A6"
       />
   </Target>
 
   <Target Name="ValidateGenericWix">
      <Error Condition="'$(OutputName)'==''" Text="REQUIRED OutputName. used for naming msi. i.e. [OutputName].msi"/>
      <Error Condition="'$(OutputType)'==''" Text="REQUIRED OutputType. Package(.msi), Module(.msm), Library(.wixlib)"/>
      <Error Condition="'$(WixToolPath)'==''" Text="REQUIRED WixToolPath. Provide path to where Wix is installed."/>
      <Error Condition="'$(MsiOutputPath)'==''" Text="REQUIRED MsiOutputPath. Where msi will be created to."/>
      <Error Condition="'$(LicenseFile)'==''" Text="REQUIRED LicenseFile. EULA for the company."/>
      <Error Condition="'$(WixSetupFile)'==''" Text="REQUIRED WixSetupFile. Main entry point of Wix script we can swap it with different Setup to compltely change the behavior of Wix script."/>
      <Error Condition="'$(WixUIDialogBmp)'==''" Text="REQUIRED WixUIDialogBmp. Branding company setup."/>
      <Error Condition="'$(WixUIBannerBmp)'==''" Text="REQUIRED WixUIBannerBmp. Branding company setup."/>
      <Error Condition="'$(ApplicationName)'==''" Text="REQUIRED ApplicationName"/>
      <Error Condition="'$(InstallVersion)'==''" Text="REQUIRED InstallVersion"/>
      <Error Condition="'$(CompanyName)'==''" Text="REQUIRED CompanyName."/>
      <Error Condition="'$(WixDirectoryFregment)'==''"  Text="REQUIRED WixDirectoryFregment. Every application provide its own directory, registry, components."/>
      <Error Condition="'$(UpgradeCode)'==''"  Text="REQUIRED UpgradeCode."/>
      <Error Condition="'$(ProductId)'==''"  Text="REQUIRED ProductId."/>
      <Error Condition="'$(CompanyUID)'==''"  Text="REQUIRED CompanyUID. It is used as unique required by Wix"/>
      <Error Condition="'$(Compressed)'==''"  Text="REQUIRED Compressed. Set it to no if admin install needed for patching."/>
      <Error Condition="'$(Description)'==''"  Text="REQUIRED Description."/>
   </Target>
 
</Project>


Unit Test

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Diagnostics;
using System.Management;
using Microsoft.Build.BuildEngine;
using Microsoft.Win32;
using System.Security.Cryptography;
using System.Xml.Serialization;
using System.Collections;
using NUnit.Core;
using NUnit.Framework;
 
namespace GenericWixTest
{
    [TestFixture]
    public class GenericWixScriptTest
    {
        private Engine _msbuild;
        private string _msbuildLog;
        private string _msbuildScript;
        private string _msiUnitTest;
        private bool _msiInstalled;
 
        [TestFixtureSetUp]
        public void Init()
        {
            _msiInstalled = false;
            _msbuild = new Engine();
            // Point to the path that contains the .NET Framework 3.5 CLR and tools
            _msbuild.BinPath = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory();
 
            _msbuildLog = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "msbuild_wix_script.log");
            _msbuildScript = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "generic.wixproj");
            _msiUnitTest = AppDomain.CurrentDomain.BaseDirectory + @"\unittestmsioutput\UnitTest.msi";
        }
 
        [SetUp]
        public void InitPerTest()
        {
            _msiInstalled = false;
            Clean(_msbuildLog, _msiUnitTest);
        }
 
        [TearDown]
        public void DisposePerTest()
        {
        }
 
        [TestFixtureTearDown]
        public void Dispose()
        {
            if (_msbuild != null)
                _msbuild.UnregisterAllLoggers();
            if (_msiInstalled)
            {
                Process installerProcess = System.Diagnostics.Process.Start("msiexec", "/qn /x " + _msiUnitTest);
                installerProcess.WaitForExit();
            }
        }
 
        [Test]
        public void TestGenericWixUpgradeScript()
        {
            FileLogger logger = new FileLogger();
            logger.Parameters = "logfile=" + _msbuildLog;
            _msbuild.RegisterLogger(logger);
 
            // Variables required by Wix script
            string version = "2.0.0";
            string appName = "Unit Test Application";
            string companyName = "Unit Test Company";
            string companyUID = "UnitTestCompany";
            string wixDirectoryFregment = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"WxsAndSource\Files.wxs");
            string licenseFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"WxsAndSource\license.rtf");
            string wixSetupFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"WxsAndSource\Setup.wxs");
            string msiOutputPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"unittestmsioutput\");
            string outputType = "package";
            string wixUIDialogBmp = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"WxsAndSource\BackGround.bmp");
            string wixUIBannerBmp = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"WxsAndSource\Banner.bmp");
            string upgradecode = Guid.NewGuid().ToString();
            string productId = "{1479ADCA-EA02-4EEA-980F-F7E52D2A6509}";
            string description = "Unit Test Description";
            string compressed = "yes";
            // Wix build test
            string wixVersion = "3.0.2925.0";
            BuildPropertyGroup pg = new BuildPropertyGroup();
            DirectoryInfo dir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory + string.Format(@"\..\..\..\wix-{0}-binaries\", wixVersion));
 
            string[] a = upgradecode.Split(';');
            // These are REQUIRED by Wix script
            pg.SetProperty("WixToolPath", dir.FullName);
            pg.SetProperty("OutputName", "UnitTest");
            pg.SetProperty("InstallVersion", version);
            pg.SetProperty("ApplicationName", appName);
            pg.SetProperty("CompanyName", companyName);
            pg.SetProperty("CompanyUID", companyUID);
            pg.SetProperty("WixDirectoryFregment", wixDirectoryFregment);
            pg.SetProperty("LicenseFile", licenseFile);
            pg.SetProperty("WixSetupFile", wixSetupFile);
            pg.SetProperty("MsiOutputPath", msiOutputPath);
            pg.SetProperty("OutputType", outputType);
            pg.SetProperty("WixUIDialogBmp", wixUIDialogBmp);
            pg.SetProperty("WixUIBannerBmp", wixUIBannerBmp);
            pg.SetProperty("UpgradeCode", upgradecode);
            pg.SetProperty("ProductId", productId);
            pg.SetProperty("Compressed", compressed);
            pg.SetProperty("Description", description);
            bool success = _msbuild.BuildProjectFile(_msbuildScript, new string[] { "BuildWix" }, pg);
            Assert.AreEqual(true, success, "msbuild failed.");
            Assert.AreEqual(true, File.Exists(_msiUnitTest));
 
 
            // msi install test
            string installedLicenFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), string.Format("{0}/license.rtf", appName));
            Process installerProcess;
            installerProcess = System.Diagnostics.Process.Start(_msiUnitTest, "/qn");
            installerProcess.WaitForExit();
            Assert.AreEqual(true, File.Exists(installedLicenFile));
            _msiInstalled = true;
 
 
            // Check installed version in registry
            RegistryKey regKey = Microsoft.Win32.Registry.LocalMachine;
            RegistryKey subKey1 = regKey.OpenSubKey(string.Format("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{0}", "{1479ADCA-EA02-4EEA-980F-F7E52D2A6509}"));
            Assert.AreEqual(true, subKey1 != null);
            Assert.AreEqual(version, subKey1.GetValue("DisplayVersion"));
            Assert.AreEqual(appName, subKey1.GetValue("DisplayName"));
 
            // msi uninstall test
            installerProcess = System.Diagnostics.Process.Start("msiexec", "/qn /x " + _msiUnitTest);
            installerProcess.WaitForExit();
            Assert.AreEqual(false, File.Exists(installedLicenFile));
 
            // Check to make sure regitry entries are clean
            subKey1 = regKey.OpenSubKey(string.Format("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{0}", "{1479ADCA-EA02-4EEA-980F-F7E52D2A6509}"));
            Assert.AreEqual(null, subKey1, "Uninstall did not clean the registry entries");
        }
 
        [Test]
        public void TestGenericWixPatchScript()
        {
            List<WxsKeyValue> list = new List<WxsKeyValue>();
            list.Add(new WxsKeyValue("1key", "1val"));
            list.Add(new WxsKeyValue("2key", "2val"));
            XmlSerializer ser = new XmlSerializer(typeof(List<WxsKeyValue>));
            string file = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "text.xml");
            using (Stream stream = File.Open(file, FileMode.Create))
            {
                ser.Serialize(stream, list);
            }
            using (Stream read = File.Open(file, FileMode.Open))
            {
                list = (List<WxsKeyValue>) ser.Deserialize(read);
            }
            WxsKeyValue found = list.Find(new WxsKeyMatch("1key").Match);
            Assert.AreEqual(list[0], found);
        }
 
        private string ComputeHash(string plainText, string hashAlgorithm, byte[] saltBytes)
        {
            // If salt is not specified, generate it on the fly.
            if (saltBytes == null)
            {
                // Define min and max salt sizes.
                int minSaltSize = 4;
                int maxSaltSize = 8;
 
                // Generate a random number for the size of the salt.
                Random random = new Random();
                int saltSize = random.Next(minSaltSize, maxSaltSize);
 
                // Allocate a byte array, which will hold the salt.
                saltBytes = new byte[saltSize];
 
                // Initialize a random number generator.
                RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
 
                // Fill the salt with cryptographically strong byte values.
                rng.GetNonZeroBytes(saltBytes);
            }
 
            // Convert plain text into a byte array.
            byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);
 
            // Allocate array, which will hold plain text and salt.
            byte[] plainTextWithSaltBytes =
                    new byte[plainTextBytes.Length + saltBytes.Length];
 
            // Copy plain text bytes into resulting array.
            for (int i = 0; i < plainTextBytes.Length; i++)
                plainTextWithSaltBytes[i] = plainTextBytes[i];
 
            // Append salt bytes to the resulting array.
            for (int i = 0; i < saltBytes.Length; i++)
                plainTextWithSaltBytes[plainTextBytes.Length + i] = saltBytes[i];
 
            // Because we support multiple hashing algorithms, we must define
            // hash object as a common (abstract) base class. We will specify the
            // actual hashing algorithm class later during object creation.
            HashAlgorithm hash;
 
            // Make sure hashing algorithm name is specified.
            if (hashAlgorithm == null)
                hashAlgorithm = "";
 
            // Initialize appropriate hashing algorithm class.
            switch (hashAlgorithm.ToUpper())
            {
                case "SHA1":
                    hash = new SHA1Managed();
                    break;
 
                case "SHA256":
                    hash = new SHA256Managed();
                    break;
 
                case "SHA384":
                    hash = new SHA384Managed();
                    break;
 
                case "SHA512":
                    hash = new SHA512Managed();
                    break;
 
                default:
                    hash = new MD5CryptoServiceProvider();
                    break;
            }
 
            // Compute hash value of our plain text with appended salt.
            byte[] hashBytes = hash.ComputeHash(plainTextWithSaltBytes);
 
            // Create array which will hold hash and original salt bytes.
            byte[] hashWithSaltBytes = new byte[hashBytes.Length +
                                                saltBytes.Length];
 
            // Copy hash bytes into resulting array.
            for (int i = 0; i < hashBytes.Length; i++)
                hashWithSaltBytes[i] = hashBytes[i];
 
            // Append salt bytes to the result.
            for (int i = 0; i < saltBytes.Length; i++)
                hashWithSaltBytes[hashBytes.Length + i] = saltBytes[i];
 
            // Convert result into a base64-encoded string.
            string hashValue = Convert.ToBase64String(hashWithSaltBytes);
 
            // Return the result.
            return hashValue;
        }
 
 
        private void RecurseDirectories(DirectoryInfo sourceDir, StringBuilder wixDirectory, StringBuilder wixFeature)
        {
            string src;
            string longName;
            string shortName;
            foreach (FileInfo file in sourceDir.GetFiles())
            {
                src = file.FullName;
                longName = file.Name;
                shortName = (file.Name.Length > 8 ? file.Name.Substring(0, 5) : file.Name) + (file.Extension.Length > 3 ? file.Extension.Substring(0, 3) : file.Extension);
            }
            foreach (DirectoryInfo dir in sourceDir.GetDirectories())
            {
                RecurseDirectories(dir, wixDirectory, wixFeature);
            }
        }
 
 
        private void Clean(params string[] files)
        {
            foreach (string file in files)
            {
                if (File.Exists(file))
                {
                    File.Delete(file);
                }
            }
        }
    }
    public class WxsKeyValue
    {
        public string WxsKey;
        public string CompId;
        public WxsKeyValue()
        {
        }
        public WxsKeyValue(string wxsKey, string wxsValue) 
        {
            WxsKey = wxsKey;
            CompId = wxsValue;
        }
    }
 
    public class WxsKeyMatch
    {
        private string _key;
 
        public WxsKeyMatch(string key)
        {
            _key = key;
        }
 
        public Predicate<WxsKeyValue> Match
        {
            get { return IsMatch; }
        }
 
        private bool IsMatch(WxsKeyValue wxsKeyValue)
        {
            if (_key.ToLower().Equals(wxsKeyValue.WxsKey.ToLower()))
            {
                return true;
            }
            return false;
        }
    }
 
    public class WxsComparer<T> : IEqualityComparer<WxsKeyValue>
    {
        public bool Equals(WxsKeyValue x, WxsKeyValue y)
        {
            if (x.WxsKey.ToLower().Equals(y.WxsKey.ToLower()))
                return true;
            else
                return false;
        }
 
        public int GetHashCode(WxsKeyValue obj)
        {
            return this.GetHashCode();
        }
    }
}


So what is the point of this unit test? This unit test will programmatically execute following msbuild command:

msbuild /t:BuildWix /p:InstallVersion="2.0.0";ApplicationName="Unit Test Application" generic.wixproj

I missing other properties that are set in unit test but it is the basic idea.

Once msbuild is executed it will create UnitTest.msi. UnitTest.msi is then executed and we check the registry to make sure it is installed properly.

SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{1479ADCA-EA02-4EEA-980F-F7E52D2A6509}


Learning Wix itself is difficult but Sayed did great job of explaining some of the important features in his article http://msdn.microsoft.com/en-us/magazine/cc163456.aspx. Setup.wxs contains bare bone minimum required to create MSI. Important thing to note is that Directory and Feature is nothing more than a placeholder.
On the unit test Files.wxs is used and it was hard coded but in real world say you have 100s of files say for web site it would be time consuming to add everything by hand.

<directory id="TARGETDIR" name="SourceDir">
<featureref id="Complete">

Tallow.exe comes with Wix toolset that can generate Directory fragements. See http://blogs.dovetailsoftware.com/blogs/kmiller/archive/2007/12/10/generating-wix-xml-using-tallow.aspx. I choose to use t4 template you get much more control and flexibility that can integrate well with MSBuild.

create_files_wxs.tt

<#@ template language="C#" hostspecific="true" #>
<#@ output extension=".xml" #>
<#@ import namespace="System" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ assembly name="System.Xml" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Security.Cryptography" #>
<#  
  /* 
  ****************************************************************************
  Purpose:
    Purpose of this script is to generate wix directory fragments 
    containing components and file and Feature fragment by crawling 
    through directories. So it can be used in Continous Integration.
 
    This script also handles synchronizing and using existing component 
    guid if not found in wxs key value xml file. Otherwise it will generate 
    component guid and save it to the file.
 
    This script assumes that the user is always doing MAJOR UPGRADE!
 
  [IN]
    ASSUMES delimited number of items sourceAppTypes, ports, sourceDirs, 
    outputFiles, and wxsKeyValuefiles must match.
 
    sourceRootDirName - Root directory name (i.e. SomeProject.vob). It is used when 
      generating wix source to use only the relative path not absolute path.
      Becase absolute path changes from machine to machine.
    sourceAppTypes - WebSite, WebService, WinService, WinApp
    sourceDirs - directories where the script needs to crawl seperated by ;
    outputFiles - creates output file per each directories it crawls
 
    Here is typical input.
 
    string   sourceRootDirName = @"\SomeTrunkDirName;
    string[] sourceAppTypes = @"WebSite;WebService;WinService".Split(';');
    string[] sourceDirs = 
          @"c:\somedir\temp\website1\version_1.0.5\source; 
          c:\somedir\temp\webservice1\version_1.0.1\source; 
          c:\somedir\temp\winservice1\version_1.0.20\source; 
          ".Split(';');
    string[] outputFiles =
          @"c:\somedir\temp\website1\version_1.0.5\generatedwxs\WebSite.wxs;
           c:\somedir\temp\webservice1\version_1.0.1\generatedwxs\WebService.wxs; 
           c:\somedir\temp\winservice1\version_1.0.20\generatedwxs\WinService.wxs; 
           ".Split(';');                        
 
    Use below MSBuild task to overwrite above placeholders
 
    <FileUpdate Files="$(CreateFilesWxsT4)"
          Regex="\[--SOURCEROOTDIRNAME--\]"
          ReplacementText="$(SourceRootDirName)" 
          Condition="'@(AppProject)' != ''"/>
    <FileUpdate Files="$(CreateFilesWxsT4)"
          Regex="\[--SOURCEAPPTYPES--\]"
          ReplacementText="@(AppProject->'%(InstallType)',';')" 
          Condition="'@(AppProject)' != ''"/>
    <FileUpdate Files="$(CreateFilesWxsT4)"
          Regex="\[--SOURCEDIRECTORY--\]"
          ReplacementText="@(AppProject->'%(PackageSourceRoot)',';')" 
          Condition="'@(AppProject)' != ''"/>
    <FileUpdate Files="$(CreateFilesWxsCopyT4)"
          Regex="\[--OUTPUTFILES--\]"
          ReplacementText="@(AppProject->'%(WixDirectoryFregment)',';')"
          Condition="'@(AppProject)' != ''"/>
  ****************************************************************************
  */
 
  string sourceRootDirName = @"[--SOURCEROOTDIRNAME--]";
  string[] sourceAppTypes = @"[--SOURCEAPPTYPES--]".Split(';');
  string[] sourceDirs = @"[--SOURCEDIRECTORY--]".Split(';');
  string[] outputFiles = @"[--OUTPUTFILES--]".Split(';');
 
  for(int i = 0; i < sourceDirs.Length; i++)
  {  
    // Initialize variables
    DirectoryInfo sourceDir = new DirectoryInfo(sourceDirs[i]);  
    FileInfo outputFile = new FileInfo(outputFiles[i]);
 
    // Generate Wix fragments
    switch (sourceAppTypes[i].ToUpper())
        {
            case "WEBSITE":
            case "WEBSERVICE":
        GenerateWebSiteWxs(sourceDir, sourceRootDirName);
                break;
            case "WINAPP":
        GenerateWinAppWxs(sourceDir, sourceRootDirName);
                break;
            case "WINSERVICE":
        GenerateWinServiceWxs(sourceDir, sourceRootDirName);
                break;
        }
    SaveOutput(outputFile);
  }
#>
 
 
<#+ private void GenerateWinServiceWxs(DirectoryInfo sourceDir
                , string sourceRootDirName) { #>
<?xml version="1.0"?>
<!--
===========================================================
  Put here Any Windows Service related fragments
===========================================================
-->
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
     xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
 
  <Fragment>
 
    <?if $(var.INSTALLTYPE) = WinService ?>
 
    <DirectoryRef Id="INSTALLWINDOWSSERVICEDIR">
      <#+
    List<string> components = new List<string>();
 
    FileInfo exeFile = GetExeFile(sourceDir);
    string serviceExeFileId = "_" + exeFile.Name.Replace(".", "_");
 
    GetFiles(sourceDir, components, exeFile, sourceRootDirName);
    RecurseDirectories(sourceDir, components, sourceRootDirName);
    string longName = sourceDir.FullName.Substring(sourceDir.FullName.IndexOf(sourceRootDirName));
    string componentGuid = GetDeterministicGuid("C_WinServiceInstallation"+longName).ToString("B").ToUpper();
    #>
      <Component Id='C_WinServiceInstallation' Guid='<#= componentGuid #>' >
    <File Id='<#= serviceExeFileId #>' Name='<#= exeFile.Name #>' Source='<#= exeFile.FullName #>' KeyPath='yes' Vital='yes' Checksum='yes' DiskId='1' />   
 
        <ServiceInstall Id='<#= serviceExeFileId #>'
          DisplayName='$(var.APPLICATIONNAME)' 
          Name='<#= exeFile.Name #>'
          Interactive='no'
          ErrorControl='normal' 
          Start='auto' 
          Type='ownProcess' 
          Vital='yes'
          Description='$(var.DESCRIPTION)'
          Account='LocalSystem' />
        <ServiceControl Id='<#= serviceExeFileId #>'
          Name='<#= exeFile.Name #>'
          Start='install'
          Stop='both' 
          Remove='uninstall' 
          Wait='yes' />
 
      </Component>
 
    </DirectoryRef>
 
    <ComponentGroup Id='CG_WinService_$(var.APPLICATIONNAME)'>
      <ComponentRef Id='C_WinServiceInstallation' />
 
      <#+ GetComponents(components); #>
    </ComponentGroup>
 
    <?endif ?>
 
  </Fragment>
</Wix>
<#+ } #>
 
 
<#+ private void GenerateWinAppWxs(DirectoryInfo sourceDir
                , string sourceRootDirName) { #>
<?xml version="1.0"?>
<!--
===========================================================
  Put here any Windows Application related fragments
===========================================================
-->
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
     xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
  <Fragment>
 
    <?if $(var.INSTALLTYPE) = WinApp ?>
 
    <DirectoryRef Id="INSTALLWINDOWSAPPDIR">
      <#+
    List<string> components = new List<string>();
    GetFiles(sourceDir, components, sourceRootDirName);
    RecurseDirectories(sourceDir, components, sourceRootDirName);
    #>
    </DirectoryRef>
 
    <ComponentGroup Id='CG_WinApp_$(var.APPLICATIONNAME)'>
      <#+ GetComponents(components); #>
    </ComponentGroup>
 
    <?endif ?>
 
  </Fragment>
</Wix>
 
<#+ } #>
 
<#+ private void GenerateWebSiteWxs(DirectoryInfo sourceDir
                , string sourceRootDirName) { #>
<?xml version="1.0"?>
<!--
===========================================================
  Put here any IIS deployment related fragments here.
===========================================================
-->
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
     xmlns:util="http://schemas.microsoft.com/wix/UtilExtension"
     xmlns:iis="http://schemas.microsoft.com/wix/IIsExtension">
  <Fragment>
 
    <?if $(var.INSTALLTYPE) = WebSite Or $(var.INSTALLTYPE) = WebService ?>
 
    <iis:WebDirProperties
      Id='WEBSITE$(var.APPLICATIONNAME)DirProp'
      Read='yes'
      LogVisits='yes'
      Index='yes'
      Script='yes'
      DefaultDocuments='login.aspx,service.svc,service1.svc,service.asmx,default.aspx' />
 
    <iis:WebApplication
      Id='WEBAPP$(var.APPLICATIONNAME)'
      Name='WEBAPP$(var.APPLICATIONNAME)'
      SessionTimeout='20'
      WebAppPool='_AppPool_$(var.APPLICATIONNAME)' />
 
    <DirectoryRef Id="INSTALLWEBSITEDIR">
      <#+
      List<string> components = new List<string>
        ();
        GetFiles(sourceDir, components, sourceRootDirName);
        RecurseDirectories(sourceDir, components, sourceRootDirName);
        string longName = sourceDir.FullName.Substring(sourceDir.FullName.IndexOf(sourceRootDirName));
        string componentGuid = GetDeterministicGuid("C_WebSiteInstallation"+longName).ToString("B").ToUpper();
        string appPoolGuid = GetDeterministicGuid("C_AppPool"+longName).ToString("B").ToUpper();
        string removeInstallDirGuid = GetDeterministicGuid("C_RemoveDir"+longName).ToString("B").ToUpper();
        string certificateComponentGuid = GetDeterministicGuid("C_WEBSITE_InstallCertificate_"+longName).ToString("B").ToUpper();
        string virualDirGuid = GetDeterministicGuid("C_VirDir"+longName).ToString("B").ToUpper();
        #>
        <Component
        Id='C_AppPool_$(var.APPLICATIONNAME)'
        Guid='<#= appPoolGuid #>'
        KeyPath='yes'>
 
        <iis:WebAppPool
          Id='_AppPool_$(var.APPLICATIONNAME)'
          Name='_AppPool_$(var.APPLICATIONNAME)' />
      </Component>
 
      <?if $(var.HTTPSPORT) != no ?>
 
      <Component
        Id='C_WEBSITE_InstallCertificate_$(var.APPLICATIONNAME)'
        Guid='<#= certificateComponentGuid #>'
        KeyPath='yes'
        DiskId='1'>
 
        <iis:Certificate
          Id='_WEBSITE_InstallCertificate_$(var.APPLICATIONNAME)'
          CertificatePath='[CERTIFICATEPATH]'
          PFXPassword='[PFXPASSWORD]'
          Name='[CERTNAME]'
          Request='no'
          StoreName='personal'
          StoreLocation='localMachine' />
      </Component>
 
      <?endif ?>
 
      <?if $(var.CREATEVIRDIR) = no ?>
 
      <Component
        Id='C_WebSiteInstallation'
        Guid='<#= componentGuid #>'
        KeyPath='yes'>
 
        <iis:WebSite
          Id='WEBSITE$(var.APPLICATIONNAME)'
          Description='$(var.DESCRIPTION)'
          DirProperties='WEBSITE$(var.APPLICATIONNAME)DirProp'
          Directory='INSTALLWEBSITEDIR'
          ConfigureIfExists='yes'
          StartOnInstall='yes'
          AutoStart='yes'
          WebApplication='WEBAPP$(var.APPLICATIONNAME)'>
 
        <iis:WebAddress
          Id='AllUnassignedWebSite'
          Port='[APPLICATIONPORT]' />
 
        <?if $(var.HTTPSPORT) != no ?>
 
        <iis:WebAddress
          Id='HTTPS'
          Port='[HTTPSPORT]'
          Secure='yes' />
        <iis:CertificateRef Id='_WEBSITE_InstallCertificate_$(var.APPLICATIONNAME)' />
 
        <?endif ?>        
        </iis:WebSite>
      </Component>
 
      <?endif ?>
 
      <?if $(var.CREATEVIRDIR) != no ?>
 
      <Component
        Id='C_WebSiteInstallation'
        Guid='<#= componentGuid #>'
        KeyPath='yes'>
 
        <iis:WebSite
          Id='WEBSITE$(var.APPLICATIONNAME)'
          Description='$(var.DESCRIPTION)'
          DirProperties='WEBSITE$(var.APPLICATIONNAME)DirProp'
          Directory='INSTALLDIR'
          ConfigureIfExists='yes'
          StartOnInstall='yes'
          AutoStart='yes'
          WebApplication='WEBAPP$(var.APPLICATIONNAME)'>
 
          <iis:WebAddress
            Id='AllUnassignedWebSite'
            Port='[APPLICATIONPORT]' />
 
          <?if $(var.HTTPSPORT) != no ?>
 
          <iis:WebAddress
            Id='HTTPS'
            Port='[HTTPSPORT]'
            Secure='yes' />
 
          <iis:CertificateRef
            Id='_WEBSITE_InstallCertificate_$(var.APPLICATIONNAME)' />
 
          <?endif ?>
        </iis:WebSite>
      </Component>
 
      <Component
        Id='C_VirDir'
        Guid='<#= virualDirGuid #>'
        KeyPath='yes'>
 
        <iis:WebVirtualDir
          Id='VirDir$(var.APPLICATIONNAME)'
          Alias='$(var.APPLICATIONNAME)'
          Directory='INSTALLWEBSITEDIR'
          WebSite='WEBSITE$(var.APPLICATIONNAME)'
          DirProperties='WEBSITE$(var.APPLICATIONNAME)DirProp'
          WebApplication='WEBAPP$(var.APPLICATIONNAME)'
               >
        </iis:WebVirtualDir>
      </Component>
 
      <?endif ?>
 
      <Component
        Id="IISRestart"
        Guid="{1100394D-E6DA-4885-B3D0-4A269A272F86}"
        KeyPath="yes">
 
        <ServiceControl
          Id='RestartIIS'
          Name='W3SVC'
          Stop='both'
          Start='both'/>
      </Component>
 
      <Component
        Id="C_RemoveDir$(var.APPLICATIONNAME)"
        Guid="<#= removeInstallDirGuid #>">
 
        <RemoveFolder
          Id="_RemoveDir$(var.APPLICATIONNAME)"
          On="uninstall"
          Directory="INSTALLWEBSITEDIR" />
      </Component>      
    </DirectoryRef>
 
    <ComponentGroup Id='CG_WebSite_$(var.APPLICATIONNAME)'>
      <ComponentRef Id='C_WebSiteInstallation' />
      <?if $(var.CREATEVIRDIR) != no ?>
 
      <ComponentRef Id='C_VirDir' />
 
      <?endif ?>
      <ComponentRef Id='C_AppPool_$(var.APPLICATIONNAME)' />
      <ComponentRef Id='C_RemoveDir$(var.APPLICATIONNAME)' />
      <ComponentRef Id='IISRestart' />
      <?if $(var.HTTPSPORT) != no ?>
 
      <ComponentRef Id='C_WEBSITE_InstallCertificate_$(var.APPLICATIONNAME)' />
 
      <?endif ?>      
      <#+ GetComponents(components); #>
    </ComponentGroup>
 
    <?endif ?>
 
    <!--
    <iis:WebApplicationExtension Extension="ad" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="adprototype" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="asax" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="ascx" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="ashx" CheckPath="no" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="asmx" CheckPath="no" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="aspx" CheckPath="no" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="axd" CheckPath="no" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="browser" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="cd" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="compiled" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="config" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="csproj" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="dd" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="exclude" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="java" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="jsl" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="ldb" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="ldd" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="lddprototype" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="ldf" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="licx" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="master" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="mdb" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="mdf" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="msgx" CheckPath="no" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="refresh" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="rem" CheckPath="no" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="resources" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="resx" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="sd" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="sdm" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="sdmDocument" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="sitemape" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="skin" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="soap" CheckPath="no" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="svc" CheckPath="no" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="vb" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="vbproj" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="vjsproj" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="vsdisco" CheckPath="no" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    <iis:WebApplicationExtension Extension="webinfo" CheckPath="yes" Script="yes" Executable="[WindowsFolder]Microsoft.NET\Framework\$(var.ASPNETVERSION)\aspnet_isapi.dll" Verbs="GET,HEAD,POST" />
    -->
 
  </Fragment>
</Wix>
 
<#+ } #>
 
<#+ 
  // Using directory name is NOT best Practice because
  // other application might have same directory name.
  // Generating guid and using guid is only good if
  // same guid is used instead of generating new one
  // because generating new guid every build will not
  // allow MSI to be patched or Upgraded.
  // wxsKeyValues contains Key using directory name
  // value as guid from previously created.
  private void RecurseDirectories(DirectoryInfo sourceDir
                  , List<string> components
                  , string sourceRootDirName) 
  {
    string longName;
    string name;
    string dirid;
    foreach (DirectoryInfo dir in sourceDir.GetDirectories())
    {
      longName = dir.FullName.Substring(dir.FullName.IndexOf(sourceRootDirName));
      dirid = "_" + GetDeterministicGuid(longName).ToString("N").ToUpper();
      name = dir.Name;
 
      WriteLine(string.Format("<Directory Id='{0}' Name='{1}'>", dirid, name));
      GetFiles(dir, components, sourceRootDirName);
      RecurseDirectories(dir, components, sourceRootDirName);
      WriteLine("</Directory>");
    }
  }
#>
 
 
<#+  
  // Read comments from RecurseDirectories.
  // It is same reason as for directory and applies to file.
  // We create component per file because it allows
  // flexibility when doing patching or
  // upgrading. Also eliminates naming conflict.
  private void GetFiles(DirectoryInfo sourceDir
              , List<string> components
              , FileInfo exclude
              , string sourceRootDirName)
  {
    string componentGuid;
    string compId;
    string fileId;
    foreach (FileInfo file in sourceDir.GetFiles())
    {
      if(exclude == null || exclude.FullName.ToUpper() != file.FullName.ToUpper())
      {
        string src = file.FullName;
 
        string longName = file.FullName.Substring(file.FullName.IndexOf(sourceRootDirName));
        Guid guid = GetDeterministicGuid(longName);
 
        componentGuid = guid.ToString("B").ToUpper();
        compId = "C__" + guid.ToString("N");
        fileId = "_" + guid.ToString("N");
        string name = file.Name;
 
        WriteLine(string.Format("<Component Id='{0}' Guid='{1}' DiskId='1'>", compId, componentGuid));
        WriteLine(string.Format("<File Id='{0}' Name='{1}' Vital='yes' KeyPath='yes' Checksum='yes' Source='{2}' />", fileId, name, src));
        WriteLine("</Component>");
 
        components.Add(compId);
      }
    }
  }
#>
 
<#+ private void GetFiles(DirectoryInfo sourceDir
              , List<string> components
              , string sourceRootDirName)
  {
    GetFiles(sourceDir, components, null, sourceRootDirName);
  }
#>
 
<#+ 
  private FileInfo GetExeFile(DirectoryInfo sourceDir) 
  {
    FileInfo exeFile = null;
    foreach (FileInfo file in sourceDir.GetFiles())
    {
      if (!string.IsNullOrEmpty(file.Extension) && file.Extension.ToUpper().Equals(".EXE"))
      {
        exeFile = file;
        break;
      }
    }
    return exeFile;
  } 
#>
 
<#+ 
  private void GetComponents(List<string> components) 
  {
    foreach(string str in components)
    {
      WriteLine(string.Format("<ComponentRef Id='{0}'/>", str));
    }
  } 
#>
 
<#+
  private void SaveOutput(FileInfo outputFile)
  {
    FileInfo file = new FileInfo(outputFile.FullName);
    if(!file.Directory.Exists)
      file.Directory.Create();
    File.WriteAllText(outputFile.FullName, this.GenerationEnvironment.ToString());
    this.GenerationEnvironment.Remove(0, this.GenerationEnvironment.Length);
  }
#>
 
<#+
  private Guid GetDeterministicGuid(string input)
  {
    //use MD5 hash to get a 16-byte hash of the string: 
    MD5CryptoServiceProvider provider = new MD5CryptoServiceProvider();    
    byte[] inputBytes = Encoding.Default.GetBytes(input);
    byte[] hashBytes = provider.ComputeHash(inputBytes);
 
    //generate a guid from the hash: 
    Guid hashGuid = new Guid(hashBytes);
 
    return hashGuid;
  }
#>


Above t4 template generates Files.wxs which is fragmented Wix script that contains Directory and Feature created from recursing through source output directory. t4 templates are basically scripting language using c#. It has very similar characteristics as CodeSmith and Velocity template.

1) In order to execute above t4 template. Copy and paste above scripting code to file and save file as createwxs.tt.
2) Change [--SOURCEDIRECTORY--] and place it with any directory.
3) Change [--OUTPUTFILES--] and place it with output file path.
4) Change [--ROOTDIRNAME--] and place it with where root of the source directory starts. (i.e. for c:\repos\myproject\trunk\src\app\myproject.webapp\ root directory name would be "src\app\")
5) Then execute
C:\Program Files\Common Files\Microsoft Shared\TextTemplating\1.2\TextTransform.exe -out File.wxs createwxs.tt. It will crawl through all the directories and create Directory and Feature and creates wxs file.

Rob made some correction to my original t4 template and I made the modification to use consitent component Guid. And with Elton's help I am now able to use deterministic guid deterministic guid which simplified my original approach! I tested on our Continuous Integration build and it's been working great.

10 comments:

Rob Mensching said...

Very nice blog post. However, your last example using the scripting to generate Fragments for your files has a very big bug in it. You are generating new Guids for the Components with every build. To correctly use the Windows Installer, the Guids must remain stable. Also, using the file system to generate the ShortName is dangerous because different machines may generate different short names and that will break the Component Rules as well.

You won't see problems until you start trying to do patching and upgrading. Thats what makes this such a nasty problem.

NewAgeSolution said...

I did some research using http://blogs.msdn.com/robmen/archive/2003/10/18/56497.aspx and I decided to migrate to Wix 3.0 instead of using Wix 2.0.

In t4 template I provided GetShortName method for creating shortnames using algorithm provided in http://blogs.msdn.com/derekc/archive/2006/02/13/531510.aspx for those who can not migrate to Wix 3.0.

As for UpgradeCode I decide to backup everything to Staging Directory including Files.wxs autogenerated by t4 template which is used later if specific version in staging directory needs patching or upgrading to keep the component Guid consitent.

Also, to avoid making mess out of development servers by installing and uninstalling on every build. I decide to use Msizap http://msdn.microsoft.com/en-us/library/aa370523.aspx to clean things up after uninstalling.
Hopely this will work out.

Thanks for pointing this out Rob. It got me thinking lots of things.

NewAgeSolution said...

After long struggle to integrate MSI package generation and installation into continuous build process, updated t4 teample, msbuild script that generates msi without generating new guid for the components. I added sych ability to t4 template script. I tested on building multiple versions and I was able to perform Major upgrades. This is only assumption on t4 template

elton said...

Useful stuff here - thanks. You can get around the Guid issue by generating Guids from the long name. This may be helpful:

http://geekswithblogs.net/EltonStoneman/archive/2008/06/26/generating-deterministic-guids.aspx

- as long as the strings stay the same between builds, they will always generate the same Guid.

Rob Mensching said...

Using major upgrades to verify Component Rule violations is not sufficient. Try creating patches for a few builds adding and removing files along the way. You'll see how things break down quickly.

Also, be very careful about the input to the function. The WiX toolset can generate stable GUIDs based off of the *target* path since the source path can vary greatly.

Tommy Norman said...

Rob, Does the t4 template above reflect all the corrections you have mentioned in these comments? I am new to this type of templating and I am getting several errors. I'd really like to include this in our build process.

NewAgeSolution said...

Tommy,

I put up updated version of T4 template above.

I have many projects currently using that exact same T4 templates for creating wix files and it's been working great.

Let me know if you have any questions or problem.

Anonymous said...

Useful post, however if i copy and paste the t4 script I get a syntax error. I have tried getting the temporary cs file into vs.net but this doesn't have the same issue.

Can you copy and paste the content from the site into your editor and see if you get the same behaviour?

Many thanks.

NewAgeSolution said...

Make sure to replace variables in t4 template

string sourceRootDirName = @"[--SOURCEROOTDIRNAME--]";

string[] sourceAppTypes = @"[--SOURCEAPPTYPES--]".Split(';');

string[] sourceDirs = @"[--SOURCEDIRECTORY--]".Split(';');

string[] outputFiles = @"[--OUTPUTFILES--]".Split(';');

Anonymous said...

Hi.

Thanks, that was not the problem.

The problem is that when you copy the text from your browser you get some spaces in the fragments- this causes a compilation error as apparently there is a bug in t4

e.g.
<#+ } #>
[space here]
<#+ private void GenerateWebSiteWxs(DirectoryInfo sourceDir
, string sourceRootDirName) { #>


This post lead me to the answer:

http://www.olegsych.com/2008/09/t4-tutorial-troubleshooting-code-generation-errors/

hope this helps someone else resolve the issue

PS copying from Google chrome seems to work the best- firefox copies an extra space per line for some reason.