Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / IIS

Creating a WIX Installer for ASP.NET Web Applications

4.94/5 (73 votes)
12 Nov 2010CPOL15 min read 523K   8.7K  
Example of creating a WIX installer for an ASP.NET Web application.

intro.jpg

Table of Contents

Introduction

A couple of months ago I started to write an installer for a web application. I decided to learn how to write it in WIX because I've heard positive reviews about it. It wasn't so easy at the very beginning but the time I've spent in that platform is very valuable for me. Since then I realized that having an installer is an important part of the software delivery process. After writing an installer for a few projects, this activity became easy and fast. In this article, I will introduce code for creating an installer for a web application in WIX and I will show you how to compile it using commands from the command line. I will also introduce an msbuild script for creating an installer for a web application, starting from publishing a web application through harvesting the application content and finishing by compiling the installer. You will understand the process and thus you will be able to extend it according to the demands of your web application.

Why do we need an installer for a web application?

In my professional career, I've been mostly developing ASP.NET web applications. Not all of them, but most of them. The software delivery process was always pushed to be a secondary activity and therefore underestimated by almost all of the team members. Not only in the early days but even today I meet good programmers with the vision that building an installer is not necessary. Xcopy (robocopy) will always work well, and raised issues will be solved ad-hoc in the production environment. But that's not professional, is it? Well, if you are creating a blog page for you or a friend of yours which should be deployed to a hosting service, then you probably don't need an installer for it. You will probably have no option to execute the installer on the hosting service because of lacking privileges. The only thing you will have available is FTP access. So, why do we need to write an installer for a web application anyway? I would consider writing an installer for a web application which should be installed more than once and by someone else than the author of the web application. I would also consider writing an installer for a web application if there is continuous integration or a continuous delivery process as the part of the development. To be clear about that, have a look at the two following figures.

b01.jpg

Figure B01

Figure B01 shows an example which should be familiar to those who are working in a large company. Developers create software and administrators take care of the servers. They don't know each other and the company has high fluctuation. In this case, administrators get an installer from developers and install software with no coordination with developers. If the installer is written really well, then minimum documentation for administrators is required. If a server crash occurs after a year, the administrator will be able to easily install the application again and won't have to contact the developer who is probably working somewhere else because he/she got a better offer. Another example where the installer is a great option is when doing continuous integration. In that case, you usually want to redeploy your application during night side by side with a nightly build. This situation is shown by Figure B02.

b02.jpg

Figure B02

A little theory at the beginning of the article has been fulfilled, so let's see some code.

Creating a web application

The installer needs something as its input. For that reason, we need to create a simple web application. Of course, I have created one. You can find it in the MyWeb directory in the source code. It's very simple, contains just a single web page. Despite its simplicity, I placed the web application into a solution file of Visual Studio in order to be easily extended by custom libraries. The code for creating an installer is expecting a solution with a web application as an input.

Creating WIX code

In this section, I will introduce code for creating an installer in WIX. I am using version 3.0 of WIX. I found this code being very universal during several projects I've been developing. I mean this is the template I always start with. As in many things, nothing's perfect, so I modify this template from project to project to be better and better. I do not say that the current version is the best, but works well for me. I'm expecting to alter the code in this article in future if I modify some part to be written better. The installer for a web application is like any other and could not be written universally for all web applications. Though, take it as an example which could be extended according to the demands of your web application. I don't want to repeat what is already written in other well written articles, thus I will not go into the very details of the WIX code. I'm expecting the reader to know the basics of writing an installer using WIX. A very nice resource for WIX is the official tutorial [1]. The source code contains comments for better orientation. The template I'm going to introduce now is divided into five files, as you can see in Figure C01.

c01.jpg

Figure C01 - Block scheme of the source files for an installer.
File NameDescription
ConfigurationInitializeDeclaration of variables used in installer.
ProductMain file. Defining the target directory structure, main UI, features, and so on.
IISConfigurationDoes the demanded changes in IIS.
UIDialogsDefinition of custom UI screens.
MyWebUIDefinition of the order of the UI screens.
WebSiteContentGenerated code. Definition of the files from the web application output.

Before describing the code, I should say what the installer exactly does. The visual representation could be seen on Figure C02.

C02.jpg

Figure C02 - Steps done by the installer

As you can see from Figure C02, the installer does steps which are needed for almost every web application. It is obvious you need to copy your application to some directory on the target computer. You of course need to set up IIS in order to make the web application available for clients. The step about registering the web site to run under ASP.NET 2 is requested especially when you are deploying on IIS 6, and finally changing the configuration string is a feature that I have always needed in every web application I have ever written. The described actions need input information gathered from the person who processes the installation. Therefore as part of the installer is a set of UI screens which have to be filled before the installer begins processing the installation.

Note: As you may have noticed, the installer creates an application pool. For that action, the installer asks for an account under which the pool will be executed. Please read [6] for understanding what the account needs. It took me some minutes to figure out why the application pool created by the installer fails to start on particular servers.

ConfigurationInitialize.wxi - Definition of variables

This file is an include file, thus the structure is quite a bit different from the others.

XML
<?xml version="1.0" encoding="utf-8"?>
<Include>
    <!-- +++++++++++++++++++ VIRTUAL_DIR_VAL property initialize +++++++++++++++++++ -->
    <Property Id="VIRTUAL_DIR_VAL" Value="MyWeb" />

    <!-- +++++++++++++++++++ web app name properties initialize ++++++++++++++++++++ -->
    <Property Id="WEB_APP_NAME" Value="MyWeb" />

    <!-- +++++++++++++++++++ app pool identity properties initialize +++++++++++++++ -->
    <Property Id="WEB_APP_POOL_IDENTITY_DOMAIN" Value="POOL_DOMAIN" />
    <Property Id="WEB_APP_POOL_IDENTITY_NAME" Value="account-name" />
    <Property Id="WEB_APP_POOL_IDENTITY_PWD" Hidden="yes" />
    
    <!-- +++++++++++++++++++++++++++ Connection String +++++++++++++++++++++++++++ -->
    <Property Id="CONNECTION_STRING" 
      Value="Data Source=|SERVER|;Initial Catalog=|Database name|;
             User Id=|LOGIN|;Password=|PASSWORD|;Persist Security Info=True" />
</Include>
Listing C01 - ConfigurationInitialize.wxi

As you can see on the previous listing, the file contains just an ID/VALUE collection of variables with default values. These values could be customized directly from the command line when you are launching the installation. The main reason for having those is for mapping to UI dialogs and using them for parameterization of the installation. For example, the connection string will be prompted to be specified by the user during the installation and finally written into the configuration file.

Product.wxs - the main code of the installer

This code is the main code of the installer. It could be seen in Listing C02.

XML
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
     xmlns:iis="http://schemas.microsoft.com/wix/IIsExtension"
     xmlns:util="http://schemas.microsoft.com/wix/UtilExtension"
     xmlns:netfx="http://schemas.microsoft.com/wix/NetFxExtension">
     
    <Product Id="{FA1B9338-B6F6-413e-B67F-86CA8BCED6E8}" 
             Name="MyWeb.Setup" 
             Language="1033" 
             Version="1.0.0.0" 
             Manufacturer="MyWeb.Setup" 
             UpgradeCode="{E5C9F391-5787-4fd1-81E6-D1A4A91D226D}">
        
        <Package InstallerVersion="200" Compressed="yes" />

        <Media Id="1" Cabinet="MyWeb.cab" EmbedCab="yes" />

        <!-- 
            * Variables 
         -->
        <!-- Configurable install location -->
        <PropertyRef Id="NETFRAMEWORK30_SP_LEVEL" />
        <Property Id="WIXUI_INSTALLDIR" Value="INSTALLLOCATION" />

        <!-- Creating directories -->
        <Directory Id="TARGETDIR" Name="SourceDir">
            <!-- Install stuff into program files folder. -->
            <Directory Id="ProgramFilesFolder">
                <!-- In program files create folder with name MyWeb. -->
                <Directory Id="INSTALLLOCATION" Name="MyWeb">
                    <!-- This is the folder where the website content will be located --> 
                    <Directory Id="MYWEBWEBSITE" Name="Website">
                        <!-- Continue in DirectoryRef with specific name -->
                    </Directory>
                    <!-- Here you can add another directories -->
                </Directory>
            </Directory>
        </Directory>

        <!-- Complete feature which will be installed. -->
        <Feature Id="Complete"
             Title="MyWeb - My awesome web"
             Level="1"
             Display="expand"
             ConfigurableDirectory="INSTALLLOCATION">
             
             <!-- Main content of the Complete feature. -->
            <Feature Id="MainContent"
                     Title="MyWeb Website"
                     Description="The website content"
                     Level="1">
                
                <!-- Include IIS Configuration. -->
                <ComponentGroupRef Id="MyWebIssConfiguration" />
                
                <!-- Include web content. -->
                <ComponentGroupRef Id="MyWebWebComponents" />
                
                <!-- Perform changes in the web.config file. -->
                <ComponentRef Id="WebConfigCmp" />

            </Feature>
        </Feature>

        <DirectoryRef Id="MYWEBWEBSITE">
            <!-- Component handling the web.config -->
            <Component Id="WebConfigCmp" Guid="">
                <!-- Copy web.config to MYWEBWEBSITE folder. -->
                <File Id="WebConfigFile" KeyPath="yes" 
                  Source="$(var.publishDir)\Web.config" Vital="yes" />
                <util:XmlFile Id="ModifyConnectionString"
                         Action="setValue"
                         Permanent="yes"
                         ElementPath="/configuration/connectionStrings/
                                      add[\[]@name='MyConnectionString'[\]]"
                         Name="connectionString"
                         File="[#WebConfigFile]"
                         Value="[CONNECTION_STRING]"
                         SelectionLanguage="XSLPattern"
                         Sequence="1" />
            </Component>
        </DirectoryRef>

        <!-- .NET Framework 3.0 SP 1 must be installed -->
        <Property Id="FRAMEWORKBASEPATH">
            <RegistrySearch Id="FindFrameworkDir" Root="HKLM" 
              Key="SOFTWARE\Microsoft\.NETFramework" 
              Name="InstallRoot" Type="raw"/>
        </Property>

        <Property Id="ASPNETREGIIS" >
            <DirectorySearch Path="[FRAMEWORKBASEPATH]" 
                        Depth="4" Id="FindAspNetRegIis">
                <FileSearch Name="aspnet_regiis.exe" MinVersion="2.0.5"/>
            </DirectorySearch>
        </Property>
        
        <!-- Switch ASP.NET to version 2.0 -->
        <CustomAction Id="MakeWepApp20" Directory="MYWEBWEBSITE" 
          ExeCommand="[ASPNETREGIIS] -norestart -s W3SVC/1/ROOT/[WEB_APP_NAME]" 
          Return="check"/>

        <InstallExecuteSequence>
            <Custom Action="MakeWepApp20" After="InstallFinalize">
                   ASPNETREGIIS AND NOT Installed</Custom>
        </InstallExecuteSequence>
        
        <!-- License and images -->
        <WixVariable Id="WixUILicenseRtf" Value="$(var.MyWebResourceDir)\License.rtf" />

        <!-- Specify UI -->
        <UIRef Id="MyWebUI" />
        
    </Product>
</Wix>
Listing C02 - Product.wxs

For better understanding, Listing C02 contains comments and the code is not so difficult to understand. What is a bit tricky is changing the connection string after the configuration file is copied to the desire location. This could be seen in Listing C03.

XML
<util:XmlFile Id="ModifyConnectionString"
     Action="setValue"
     Permanent="yes"
     ElementPath="/configuration/connectionStrings/
                  add[\[]@name='MyConnectionString'[\]]"
     Name="connectionString"
     File="[#WebConfigFile]"
     Value="[CONNECTION_STRING]"
     SelectionLanguage="XSLPattern"
     Sequence="1" />
Listing C03 - Customizing connection string

Because this doesn't have to be taken as basic knowledge, I point you out ti the excellent article [2] explaining this code.

IISConfiguration.wxs - configuration of IIS

From the name of the file, you can guess that this code will take care of the IIS configuration. It means creating a virtual directory and web application pool. The code could be seen in Listing C04.

XML
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
     xmlns:iis="http://schemas.microsoft.com/wix/IIsExtension"
     xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
    
    <Fragment>
        <?include ConfigurationInitialize.wxi ?>

        <!-- Install to default web site -->
        <iis:WebSite Id="DefaultWebSite" Description='Default Web Site'>
            <iis:WebAddress Id="AllUnassigned" Port="80" />
        </iis:WebSite>

        <DirectoryRef Id="MYWEBWEBSITE">
            <!-- Configuring app pool -->
            <Component Id="MyWebAppPoolCmp" Guid="" KeyPath="yes">
                <util:User Id="MyWebAppPoolUser"
                           CreateUser="no"
                           Name="[WEB_APP_POOL_IDENTITY_NAME]"
                           Password="[WEB_APP_POOL_IDENTITY_PWD]"
                           Domain="[WEB_APP_POOL_IDENTITY_DOMAIN]" />
                <iis:WebAppPool Id="MyWebAppPool"
                                Name="[WEB_APP_NAME]"
                                Identity="other"
                                User="MyWebAppPoolUser" />
            </Component>
        
            <!-- Configure virtual dir -->
            <Component Id="MyWebVirtualDirCmp" 
                   Guid="{751DEB01-ECC1-48ff-869A-65BCEE9E0528}" 
                   KeyPath="yes" >
                <iis:WebVirtualDir Id="MyWebVirtualDir" 
                          Alias="[VIRTUAL_DIR_VAL]" Directory="MYWEBWEBSITE" 
                          WebSite="DefaultWebSite">
                    <iis:WebDirProperties Id="MyWebVirtDirProperties" 
                       AnonymousAccess="no" BasicAuthentication="no" 
                       WindowsAuthentication="yes" />
                    <iis:WebApplication Id="MyWebWebApplication" 
                       Name="[VIRTUAL_DIR_VAL]" />
                </iis:WebVirtualDir>
            </Component>
        </DirectoryRef>

        <ComponentGroup Id="MyWebIssConfiguration">
            <ComponentRef Id="MyWebVirtualDirCmp" />
            <ComponentRef Id="MyWebAppPoolCmp" />
        </ComponentGroup>
        
    </Fragment>
</Wix>
Listing C04 - IIS configuration

As you may have noticed, the template uses WIX 3.0 IIS extensions to set up IIS. This is fully compatible with IIS 5 and IIS 6. Fortunately, it works with IIS 7 as well, but you have to have IIS Compatibility Management Pack installed on the target machine. However, there could be another issue when you try to use some IIS 7 features which are not available on IIS 6. As an example, consider an application pool running in Integrated mode. In cases like this, you will have to run a custom action to call AppCmd.exe in order to change it because the default mode for the application pool is Classic. See [7] for further information.

UIDialogs.wxs - Customized UI screens

As I mentioned earlier, the installer collects information from the user. For that reason, we need to add some custom screens with text boxes which have to be filled out. The code is a bite verbose as you can see in Listing C05.

XML
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
    <Fragment>
        <UI>
            <Dialog Id="IisSetupDlg" Width="370" Height="270" 
                     Title="IIS Settings - [ProductName]" NoMinimize="yes">
                <!-- Virtual Dir prompt -->
                <Control Id="VirtualDirLabel" Type="Text" X="45" Y="73" 
                   Width="100" Height="15" TabSkip="no" Text="&amp;Virtual Directory:" />
                <Control Id="VirtualDirEdit" Type="Edit" X="45" 
                   Y="85" Width="220" Height="18" Property="VIRTUAL_DIR_VAL" Text="{80}" />
                <!-- Back button -->
                <Control Id="Back" Type="PushButton" X="180" Y="243" 
                        Width="56" Height="17" Text="&amp;Back">
                    <Publish Event="NewDialog" Value="LicenseAgreementDlg">1</Publish>
                </Control>
                <Control Id="Next" Type="PushButton" X="236" Y="243" 
                   Width="56" Height="17" Default="yes" Text="&amp;Next">
                    <Publish Event="NewDialog" Value="PoolSettingsDlg">
                        <!--if settings are correct, allow next dialog-->
                        <![CDATA[VIRTUAL_DIR_VAL <> ""]]>
                    </Publish>
                </Control>
                <Control Id="Cancel" Type="PushButton" X="304" Y="243" 
                           Width="56" Height="17" Cancel="yes" Text="Cancel">
                    <Publish Event="SpawnDialog" Value="CancelDlg">1</Publish>
                </Control>
                <Control Id="BannerBitmap" Type="Bitmap" X="0" Y="0" 
                     Width="370" Height="44" TabSkip="no" Text="WixUI_Bmp_Banner" />
                <Control Id="Description" Type="Text" X="25" Y="23" 
                       Width="280" Height="15" Transparent="yes" NoPrefix="yes">
                    <Text>Please enter IIS Configuration</Text>
                </Control>
                <Control Id="BottomLine" Type="Line" X="0" Y="234" 
                      Width="370" Height="0" />
                <Control Id="Title" Type="Text" X="15" Y="6" 
                        Width="200" Height="15" Transparent="yes" NoPrefix="yes">
                    <Text>{\WixUI_Font_Title}IIS Settings</Text>
                </Control>
                <Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" />
            </Dialog>
            
            <Dialog Id="PoolSettingsDlg" Width="370" Height="270" 
                     Title="Application Pool Settings - [ProductName]" NoMinimize="yes">
                <!-- name of the application pool -->
                <Control Id="PoolNameLabel" Type="Text" X="45" Y="73" 
                    Width="100" Height="15" TabSkip="no" Text="&amp;Pool name:" />
                <Control Id="PoolNameEdit" Type="Edit" X="45" Y="85" 
                    Width="220" Height="18" Property="WEB_APP_NAME" Text="{80}" />
                <!-- domain -->
                <Control Id="DomainPoolLabel" Type="Text" X="45" Y="105" 
                    Width="100" Height="15" TabSkip="no" Text="&amp;Domain for AppPool:" />
                <Control Id="DomainPoolEdit" Type="Edit" X="45" Y="117" 
                    Width="220" Height="18" 
                    Property="WEB_APP_POOL_IDENTITY_DOMAIN" Text="{80}" />
                <!-- Login -->
                <Control Id="LoginPoolLabel" Type="Text" X="45" Y="137" 
                  Width="100" Height="15" TabSkip="no" Text="&amp;Login for AppPool:" />
                <Control Id="LoginPoolEdit" Type="Edit" X="45" Y="149" 
                  Width="220" Height="18" Property="WEB_APP_POOL_IDENTITY_NAME" Text="{80}" />
                <!-- Password -->
                <Control Id="PasswordPoolLabel" Type="Text" X="45" Y="169" 
                  Width="100" Height="15" TabSkip="no" Text="&amp;Password for AppPool:" />
                <Control Id="PasswordPoolEdit" Type="Edit" X="45" Y="181" 
                  Width="220" Height="18" Property="WEB_APP_POOL_IDENTITY_PWD" 
                  Text="{80}" Password="yes" />
                <!-- Back button -->
                <Control Id="Back" Type="PushButton" X="180" Y="243" 
                  Width="56" Height="17" Text="&amp;Back">
                    <Publish Event="NewDialog" Value="IisSetupDlg">1</Publish>
                </Control>
                <Control Id="Next" Type="PushButton" X="236" Y="243" 
                  Width="56" Height="17" Default="yes" Text="&amp;Next">
                    <Publish Event="NewDialog" Value="ConnectionStringDlg">
                        <!--if settings are correct, allow next dialog-->
                        <![CDATA[WEB_APP_NAME <> "" or WEB_APP_POOL_IDENTITY_DOMAIN <> 
                          "" or WEB_APP_POOL_IDENTITY_NAME <> "" 
                          or WEB_APP_POOL_IDENTITY_PWD <> ""]]>
                    </Publish>
                </Control>
                <Control Id="Cancel" Type="PushButton" X="304" Y="243" 
                        Width="56" Height="17" Cancel="yes" Text="Cancel">
                    <Publish Event="SpawnDialog" Value="CancelDlg">1</Publish>
                </Control>
                <Control Id="BannerBitmap" Type="Bitmap" X="0" Y="0" 
                  Width="370" Height="44" TabSkip="no" Text="WixUI_Bmp_Banner" />
                <Control Id="Description" Type="Text" X="25" Y="23" 
                       Width="280" Height="15" Transparent="yes" NoPrefix="yes">
                    <Text>Please enter AppPool Configuration for IIS</Text>
                </Control>
                <Control Id="BottomLine" Type="Line" X="0" 
                         Y="234" Width="370" Height="0" />
                <Control Id="Title" Type="Text" X="15" Y="6" 
                         Width="200" Height="15" Transparent="yes" NoPrefix="yes">
                    <Text>{\WixUI_Font_Title}Application Pool Settings</Text>
                </Control>
                <Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" />
            </Dialog>

            <Dialog Id="ConnectionStringDlg" Width="370" 
                   Height="270" Title="Database Settings - [ProductName]" NoMinimize="yes">
                <!-- Connection String -->
                <Control Id="ConnectionStringLabel" Type="Text" X="45" Y="73" 
                  Width="100" Height="15" TabSkip="no" Text="&amp;Connection String:" />
                <Control Id="ConnectionStringEdit" Type="Edit" X="45" Y="95" 
                  Width="220" Height="18" Property="CONNECTION_STRING" Text="{200}" />
                <!-- Back button -->
                <Control Id="Back" Type="PushButton" X="180" Y="243" 
                         Width="56" Height="17" Text="&amp;Back">
                    <Publish Event="NewDialog" Value="PoolSettingsDlg">1</Publish>
                </Control>
                <Control Id="Next" Type="PushButton" X="236" Y="243" 
                       Width="56" Height="17" Default="yes" Text="&amp;Next">
                    <Publish Event="NewDialog" Value="CustomizeDlg">
                        <!--if settings are correct, allow next dialog-->
                        <![CDATA[CONNECTION_STRING <> ""]]>
                    </Publish>
                </Control>
                <Control Id="Cancel" Type="PushButton" X="304" Y="243" 
                  Width="56" Height="17" Cancel="yes" Text="Cancel">
                    <Publish Event="SpawnDialog" Value="CancelDlg">1</Publish>
                </Control>
                <Control Id="BannerBitmap" Type="Bitmap" X="0" Y="0" 
                  Width="370" Height="44" TabSkip="no" Text="WixUI_Bmp_Banner" />
                <Control Id="Description" Type="Text" X="25" Y="23" 
                       Width="280" Height="15" Transparent="yes" NoPrefix="yes">
                    <Text>Please enter database configuration</Text>
                </Control>
                <Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
                <Control Id="Title" Type="Text" X="15" Y="6" 
                        Width="200" Height="15" Transparent="yes" NoPrefix="yes">
                    <Text>{\WixUI_Font_Title}Database Settings</Text>
                </Control>
                <Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" />
            </Dialog>
            
        </UI>
    </Fragment>
</Wix>
Listing C05 - Custom UI screens

Basically, three screens with definitions of labels, text boxes, buttons, and their location.

MyWebUI.wxs

This file specifies the flow of screens. The code could be seen in Listing C06.

XML
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
    <Fragment>
        <!-- MyWeb UI -->
        <UI Id="MyWebUI">

            <UIRef Id="WixUI_FeatureTree" />
            <UIRef Id="WixUI_ErrorProgressText" />

            <DialogRef Id="IisSetupDlg" />
            <!-- Injection of custom UI. -->
            <Publish Dialog="LicenseAgreementDlg" Control="Next" 
                 Event="NewDialog" Value="IisSetupDlg" 
                 Order="3">LicenseAccepted = "1"</Publish>
            <Publish Dialog="CustomizeDlg" Control="Back" 
                 Event="NewDialog" Value="ConnectionStringDlg">1</Publish>
        </UI>
    </Fragment>
</Wix>
Listing C06 - UI flow specification

WebSiteContent.wxs

Finally, we get to the last file which is the generated content of the web application. This file specifies what pages (ASPX), CSS files, JavaScript code, DLLs and so on will be included in the installer and therefore copied to the desired location during the installation. The way the file is generated is described in the next section in very detail.

The way to building an MSI package

In the previous section, we discussed some code to write an install package and now it's time to compile it. In this chapter, I will introduce commands for building an MSI package. I got used to compiling MSI packages using commands from the command line even though there is a plug-in for Visual Studio. Of course, when I started to work with WIX, I was using this Visual Studio plug-in but when time passed, I was facing issues with the plug-in. By solving the problems like why the plug-in is not working in different environments, I dug into the details of what the plug-in actually does and realized that it does almost nothing. So I decided to use the command line directly and never came back to the plug-in again. This has other benefits. For example, if you decide to work with a different technology in the future, like Java or C++, then you will still be able to write an install package because you know how to do it and you are simply not tied to Visual Studio.

The visual interpretation of the process of creating an MSI package is described in Figure D1. Basically, there are four main steps which should be achieved to create an MSI package.

d1.jpg

Figure D1 - Creating an MSI package

The first step is building a solution in Release mode in order to be sure that all DLLs are up to date. This step is followed by publishing the web application. It is the publish step that everyone knows from Visual Studio (right click on Project -> Publish). Next is a very important step which is harvesting the web application. Basically, harvesting of the output from the previous step. Lasts comes the compilation of the WIX code we've created in the second chapter.

Why is there an exclamation mark in harvesting web application content?

In WIX, harvesting files is recommended to be done once at the beginning. Afterwards, the generated content should be kept up manually. See the flow in Figure D2.

d2.jpg

Figure D2 - Recommended flow of building an installer

This approach, however, is not convenient for those who are planning to use continues integration in their project. It is always very difficult to manually keep up the output of a web application. The situation in a web application is difficult because hwne adding some files (ASPX, CSS, HTML), you have to manually update the WIX script. In continuous integration, you are expecting that your new files and therefore new web application publish will be automatically triggered by the process of building the install package and your new content will be added to the installer. In order to achieve this goal, you have to run the harvesting of the web application output every time a new project build is triggered up. But as I mentioned, that's not recommended.

What could happen if you run harvesting as part of every build? You can (but don't have to) get strange warnings related to WIX components. I will go into this chapter by introducing the commands for the process I described in the first part of this chapter. I break the rule from Figure D2 and will keep harvesting files as part of the build.

Let's see the whole bat script in listing D1.

REM Setting variables...
REM Name of the folder where the 'publish' from msbuild target will be performed
set publishFolder=publish\
REM Remove complete publish folder in order to be sure that evrything will be newly compiled
rmdir /S /Q %publishFolder%
REM Rebuild entire solution
msbuild /p:Configuration=Release /t:ReBuild ..\MyWeb\MyWeb.sln
REM Publish your web site
msbuild /t:ResolveReferences;_CopyWebApplication 
  /p:Configuration=Release;OutDir=..\..\Setup\%publishFolder%\bin\;
  WebProjectOutputDir=..\..\Setup\%publishFolder% ..\MyWeb\MyWeb\MyWeb.csproj
REM Delete debug and setup web.configs to prevent 'heat' from harveting it
del %publishFolder%web_setup.config
del %publishFolder%web.config
REM Harvest all content of published result
heat dir publish\ -dr MYWEBWEBSITE -ke -srd -cg MyWebWebComponents 
  -var var.publishDir -gg -out WebSiteContent.wxs
REM After the files are beeing harvested, copy the setup web.config 
  as regular web.config back to publish location
copy /Y ..\MyWeb\MyWeb\Web_setup.config %publishFolder%Web.config
REM At last create an installer
candle -ext WixIISExtension -ext WixUtilExtension -ext WiXNetFxExtension 
  -dpublishDir=%publishFolder% -dMyWebResourceDir=. IisConfiguration.wxs 
  Product.wxs WebSiteContent.wxs UiDialogs.wxs MyWebUI.wxs
light -ext WixUIExtension -ext WixIISExtension -ext WixUtilExtension 
  -ext WiXNetFxExtension -out bin\Release\MyWeb.Setup.msi Product.wixobj 
  WebSiteContent.wixobj UiDialogs.wixobj MyWebUI.wixobj IisConfiguration.wixobj
Listing D1 - bat script for building installer

The script is not so difficult but contains interesting things. First two commands are just setting variables and cleaning after the previous build. For a specific description, please read the comments in the script. The first command seen in Listing D2 is building the entire solution in order to be sure that all assemblies and linked projects will be up to date. It also makes sure that everything will be up to date in Release mode.

msbuild /p:Configuration=Release /t:ReBuild ..\MyWeb\MyWeb.sln
Listing D2 - Building solution in release mode

The following command seen on Listing D3 is calling the msbuild utility but for your web application and with specific targets.

msbuild /t:ResolveReferences;_CopyWebApplication 
  /p:Configuration=Release;OutDir=..\..\Setup\%publishFolder%\bin\;
  WebProjectOutputDir=..\..\Setup\%publishFolder% ..\MyWeb\MyWeb\MyWeb.csproj
Listing D3 - Publishing the web application

The result of the command is the same like clicking the button "publish" in the Visual Studio. Basically, the command will make a copy of the files which are needed to be deployed to the target machine.

The next two commands seen on Listing D4 are deleting the two configuration files from the application output. Surely you will notice that the web application has two configuration files: one for local run and one for deploy.

del %publishFolder%web_setup.config
del %publishFolder%web.config
Listing D4 - Deleting configuration files from the output

Both of them have to be deleted before the harvesting because neither of them is wanted as a file to be harvested. It is so because the web config file is registered to be in the installer in Product.wxs. Therefore it can't be harvested.

Since we have the web application output prepared to be harvested, we can execute the heat utility to do that job. See Listing D5.

heat dir %publishFolder% -dr MYWEBWEBSITE -ke -srd -cg 
    MyWebWebComponents -var var.publishDir -gg -out WebSiteContent.wxs
Listing D5 - Harvesting the web application output

After the files are harvested, we can copy the web configuration file back to the publish folder but note that the setup configuration is copied as a regular ASP.NET web configuration file. So the final name of the configuration file is web.config.

copy /Y ..\MyWeb\MyWeb\Web_setup.config %publishFolder%Web.config
Listing D6 - Copying the web configuration file

Finally, everything is ready to compile the WIX code. See Listing D7 for commands.

candle -ext WixIISExtension -ext WixUtilExtension -ext WiXNetFxExtension 
  -dpublishDir=%publishFolder% -dMyWebResourceDir=. IisConfiguration.wxs 
  Product.wxs WebSiteContent.wxs UiDialogs.wxs MyWebUI.wxs
light -ext WixUIExtension -ext WixIISExtension -ext WixUtilExtension -ext WiXNetFxExtension 
  -out bin\Release\MyWeb.Setup.msi Product.wixobj WebSiteContent.wixobj 
  UiDialogs.wixobj MyWebUI.wixobj IisConfiguration.wixobj
Lising D7 - Compile installer

From bat file to msbuild

Keeping the entire build of an installer in the bat file could be quite odd and unprofessional. Also, in continuous integration, we would like to add more features into the build process which could be difficult (but not impossible) to add into a bat file. The reason for having the previous chapter is for understanding this. Now you understand the build process, thus we can do it a little more professional and transfer it to an msbuild script. In this section, I will rewrite the bat script from the previous chapter to an msbuild script. I expect that the reader knows the basics about msbuild and how to script in it. It's really very easy but if you haven't tried it yet, I point you out to [3] for an online resource or [4] for a very nice book. The script could be seen in Listing E01.

XML
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="3.5" DefaultTargets="Build" 
       xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <WebSiteSource>..\MyWeb\MyWeb\</WebSiteSource>
        <ReleaseWebConfig>Web_setup.config</ReleaseWebConfig>
        <PublishF>publish\</PublishF>
        <Publish>..\..\Setup\$(PublishF)</Publish>
        <MsiOut>bin\Release\MyWeb.Setup.msi</MsiOut>
        <WebSiteContentCode>WebSiteContent.wxs</WebSiteContentCode>
        <WebSiteContentObject>WebSiteContent.wixobj</WebSiteContentObject>
        <WixPath></WixPath>
    </PropertyGroup>
    
    <!-- Defining group of temporary files which is the content of the web site. -->
    <ItemGroup>
        <WebSiteContent Include="$(WebSiteContentCode)" />
        <WebSiteContent Include="$(WebSiteContentFile)" />
    </ItemGroup>
    
    <ItemGroup>
        <!-- The group of web configs -->
        <WebConfigs Include="$(PublishF)Web.config" />
        <WebConfigs Include="$(PublishF)$(ReleaseWebConfig)" />
    </ItemGroup>

    <!-- The list of WIX input files -->
    <ItemGroup>
        <WixCode Include="IisConfiguration.wxs" />
        <WixCode Include="Product.wxs" />
        <WixCode Include="$(WebSiteContentCode)" />
        <WixCode Include="UiDialogs.wxs" />
        <WixCode Include="MyWebUI.wxs" />
    </ItemGroup>
    
    <!-- The list of WIX after candle files -->
    <ItemGroup>
        <WixObject Include="IisConfiguration.wixobj" />
        <WixObject Include="Product.wixobj" />
        <WixObject Include="$(WebSiteContentObject)" />
        <WixObject Include="UiDialogs.wixobj" />
        <WixObject Include="MyWebUI.wixobj" />
    </ItemGroup>
    
    <!-- Define default target with name 'Build' -->
    <Target Name="Build">
        <!-- Compile whole solution in release mode -->
        <MSBuild
            Projects="..\MyWeb\MyWeb.sln"
            Targets="ReBuild"
            Properties="Configuration=Release" />
    </Target>

    <!-- Define creating installer in another target -->
    <Target Name="CreateInstaller">
        <!-- Remove complete publish folder in order to 
             be sure that evrything will be newly compiled -->
        <RemoveDir Directories="$(PublishF)" ContinueOnError="false" />
        <MSBuild
            Projects="..\MyWeb\MyWeb\MyWeb.csproj"
            Targets="ResolveReferences;_CopyWebApplication"
            Properties="OutDir=$(Publish)bin\;WebProjectOutputDir=
                        $(Publish);Configuration=Release" />
        
        <!-- Delete debug and setup web.configs to prevent 'heat' from harveting it -->
        <!-- Delete all wix object which remain from previous build -->
        <!-- Delete all other temporary files -->
        <Delete Files="@(WebConfigs)" />
        <Delete Files="@(WixObject)" />
        <Delete Files="@(WebSiteContent)" />

        <!-- Harvest all content of published result -->
        <Exec
            Command='"$(WixPath)heat" dir $(PublishF) -dr MYWEBWEBSITE 
                     -ke -srd -cg MyWebWebComponents -var var.publishDir 
                     -gg -out $(WebSiteContentCode)'
            ContinueOnError="false"
            WorkingDirectory="." />

        <!-- After the files are beeing harvested, copy the setup web.config 
             as regular web.config back to publish location -->
        <Copy
            SourceFiles="$(WebSiteSource)$(ReleaseWebConfig)"
            DestinationFiles="$(PublishF)Web.config"
            ContinueOnError="false" />

        <!-- At last create an installer -->
        <Exec
            Command='"$(WixPath)candle" -ext WixIISExtension -ext WixUtilExtension 
                     -ext WiXNetFxExtension -dpublishDir=$(PublishF) 
                     -dMyWebResourceDir=. @(WixCode, &apos; &apos;)'
            ContinueOnError="false"
            WorkingDirectory="." />
        <Exec
            Command='"$(WixPath)light" -ext WixUIExtension -ext WixIISExtension 
                     -ext WixUtilExtension -ext WiXNetFxExtension -out 
                     $(MsiOut) @(WixObject, &apos; &apos;)'
            ContinueOnError="false"
            WorkingDirectory="." />
        
        <!-- A message at the end -->
        <Message Text="Install package has been created." />
    </Target>
    
    <!-- Optional target for deleting temporary files. Usually after build -->
    <Target Name="DeleteTmpFiles">
        <RemoveDir Directories="$(PublishF)" ContinueOnError="false" />
        <Delete Files="@(WebConfigs);@(WixObject);@(WebSiteContent)" />
    </Target>

</Project>
Listing E01 - msbuild script for creating an installer

You probably noticed that the script provides three targets. For rebuilding the solution, creating the installer, and deleting temporary files. Executing all targets could be done by the command from Listing E02.

msbuild /t:Build;CreateInstaller;DeleteTmpFiles build_setup.build
Listing E02 - Execute build script with all targets.

Conclusion

This article summarizes the steps needed for creating an installer in an automated way. What else to say at the end? Just enjoy deploying your web application using the install package :).

Please don't hesitate to leave a comment or/and vote. Thanks for reading.

References

  1. http://www.tramontana.co.hu/wix/
  2. http://blogs.technet.com/b/alexshev/archive/2009/05/27/from-msi-to-wix-part-25-installable-items-updating-xml-files-using-xmlfile.aspx
  3. http://msdn.microsoft.com/en-us/library/wea2sca5%28v=VS.90%29.aspx
  4. ISBN-10: 0735626286, ISBN-13: 978-0735626287, Inside the Microsoft Build Engine: Using MSBuild and Team Foundation Build (PRO-Developer), Authors: Sayed Ibrahim Hashimi and William Bartholomew
  5. ISBN-10: 0321601912, ISBN-13: 978-0321601919, Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation, Authors: Jez Humble, David Farley
  6. http://beyondthispoint.blogspot.com/2006/04/setting-up-iis6-application-pool.html
  7. http://forums.iis.net/p/1155735/1895551.aspx

History

  • 3 October 2010
    • Initial publish.
  • 12 November 2010
    • WIX version added.
    • Explains the compatibility for IIS 7 in section IISConfiguration.wxs - configuration of IIS.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)