« Back
in ci wix teamcity read.

Building A WiX Installer For A Windows Service In TeamCity.

Creating a Windows Service is fairly simple but then how do we properly deploy it and install it on the host machine? Perhaps it's not the developer who gets to install it on the hosting machine but somebody from a different department. We also want to be able to build the final deliverables using CI as part of our work flow so that the administrators can just grab the latest copy and enter whatever configurations they need.

As part of this workflow, I'll be using TeamCity as the Continuous Integration solution which is built by the smart guys at JetBrains. These are the same guys that built what a lot of developers consider a must have: ReSharper. We'll be building our Windows Service installer, running xUnit tests and then producing the final .msi to install and automatically start the service.

The WiX Project Type

There are a number of tools out there that provide Visual Studio projects to create an installer for services and applications. But here we'll use WiX because it's free, open source and there's a lot of documentation out there for help.

Explaining WiX is beyond the scope of this article but here's a sample Product.wxs file created in Visual Studio to create an installer for a Windows Service. The final product will be a neat Windows application that accepts a connection string as user input and all the user really has to is click next a bunch of times. If the service fails to start for some reason or the install is cancelled mid way, everything gets rolled back and removed from the file system. Nice!

<?xml version="1.0" encoding="UTF-8"?>  
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">  
    <Product Id="B661B8BD-C1D1-4565-9408-E6EDBC348D2D" 
           Name="AppEventService" 
           Language="1033" Version="1.0.0.0" 
           Manufacturer="MYCOMPANY Pty Ltd" 
           UpgradeCode="040148c0-f2ba-453c-946a-5b06c1ffaf3a">

    <Package InstallerVersion="200" 
             Compressed="yes" 
             InstallScope="perMachine"/>

    <Condition Message="You need to be an administrator to install this product.">
      Privileged
    </Condition>

        <MajorUpgrade DowngradeErrorMessage="A newer version of Application KPI Event Service is already installed." />


    <Property Id="APPCONNSTR" Secure="yes"/>
    <Media Id="1" Cabinet="AppEventService.cab" EmbedCab="yes"/>

        <Feature Id="ProductFeature" Title="Application KPI Event Service" Level="1" TypicalDefault="install" Display="expand" ConfigurableDirectory="INSTALLDIR">
            <ComponentGroupRef Id="ProductComponents" />
      <ComponentRef Id="MainExecutable"/>
      <ComponentRef Id="Application.KPI.Events"/>
      <ComponentRef Id="Config"/>
      <ComponentRef Id="EntityFramework"/>
      <ComponentRef Id="EntityFramework.SqlServer"/>
      <ComponentRef Id="Ninject"/>
        </Feature>

    <UI Id="CustomMondo">
      <UIRef Id="WixUI_Mondo" />
      <UIRef Id="WixUI_ErrorProgressText" />
      <DialogRef Id="AppSettingsDlg"/>
      <Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="AppSettingsDlg" Order="3">LicenseAccepted = "1"</Publish>
      <Publish Dialog="SetupTypeDlg" Control="Back" Event="NewDialog" Value="AppSettingsDlg">1</Publish>
    </UI>

    </Product>

  <Fragment>
    <UI>
      <Dialog Id="AppSettingsDlg" Width="370" Height="270" Title="[ProductName] Setup" NoMinimize="yes">
        <Control Id="AppConnStrLabel" Type="Text" X="45" Y="73" Width="220" Height="15" TabSkip="no" Text="&amp;Application Entities Connection String:" />
        <Control Id="AppConnStrEdit" Type="Edit" X="45" Y="85" Width="220" Height="18" Property="APPCONNSTR" Text="{280}" />
        <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 Property="CSOCONNSTR" Value="[CSOCONNSTR]">1</Publish>
            <Publish Property="PARTICIPANTSCONNSTR" Value="[PARTICIPANTSCONNSTR]">1</Publish>
            <Publish Property="SELFHOSTADDRESS" Value="[SELFHOSTADDRESS]">1</Publish>-->
          <Publish Event="NewDialog" Value="SetupTypeDlg">1</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 your connection Information</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}Connection Information</Text>
        </Control>
        <Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" />
      </Dialog>
    </UI>
  </Fragment>

  <Fragment>
    <Directory Id="TARGETDIR" Name="SourceDir">
      <Directory Id="ProgramFilesFolder" Name="PFiles">
        <Directory Id="MYCOMPANYDIR" Name="MYCOMPANY">
          <Directory Id="INSTALLDIR" Name="AppEventService"></Directory>
        </Directory>
      </Directory>
    </Directory>
  </Fragment>

  <Fragment>
    <ComponentGroup Id="ProductComponents" Directory="INSTALLDIR">
      <Component Id="MainExecutable" Guid="9DF4A64D-66B2-459D-B1C3-3F6692EB8AF1">
        <util:XmlFile Id="UpdateAppConnectionString" File="[INSTALLDIR]Application.Service.exe.config" Action="setValue" ElementPath="/configuration/connectionStrings/add[\[]@name=&quot;AppEventServiceDataContext&quot;[\]]/@connectionString" Value="[APPCONNSTR]" Permanent="yes" />
        <File Id="Application.Service.exe" Name="Application.Service.exe" DiskId="1" Source="$(var.Application.Service.TargetDir)\Application.Service.exe" KeyPath="yes" />
        <ServiceInstall Id="Application.Service.exe" Name="Application.Service" DisplayName="Application KPI Event Service" Account="LocalSystem" Start="auto" Type="ownProcess" ErrorControl="normal" Vital="yes"/>
        <ServiceControl Id="Application.Service.exe" Name="Application.Service" Remove="both" Start="install" Stop="both" Wait="yes"/>
      </Component>
      <Component Id="Application.KPI.Events">
        <File Id="Application.KPI.EventsDll" Name="Application.KPI.Events.dll" DiskId="1" Source="$(var.Application.Service.TargetDir)\Application.KPI.Events.dll" KeyPath="yes"/>
      </Component>
      <Component Id="Config" Permanent="yes">
        <File Id="ApplicationServicesConfig" Name="Application.Service.exe.config" DiskId="1" Source="$(var.Application.Service.TargetDir)\Application.Service.exe.config" KeyPath="yes" />
      </Component>
      <Component Id="EntityFramework">
        <File Id="EntityFrameworkDll" Name="EntityFramework.dll" DiskId="1" Source="$(var.Application.Service.TargetDir)\EntityFramework.dll" KeyPath="yes"/>
      </Component>
      <Component Id="EntityFramework.SqlServer">
        <File Id="EntityFramework.SqlServerDll" Name="EntityFramework.SqlServer.dll" DiskId="1" Source="$(var.Application.Service.TargetDir)\EntityFramework.SqlServer.dll" KeyPath="yes"/>
      </Component>
      <Component Id="Ninject">
        <File Id="NinjectDll" Name="Ninject.dll" DiskId="1" Source="$(var.Application.Service.TargetDir)\Ninject.dll" KeyPath="yes"/>
      </Component>
    </ComponentGroup>
  </Fragment>
</Wix>  

Things to note are: The Product Id should be a unique Guid to your app. The default Product.wxs file will have this as *.

Each <Component> item is a Dll that your application depends on. You can find and list all of these in your solution folder such as ProjectFolder\bin\Debug\ or ProjectFolder\bin\Release\

We also make a little textbox that will collect the connection string from the person performing the install and we make sure that we apply the transform to the config file. Of course we also need to make sure we move the config file along with the application. In this case it would be Application.Service.exe.config.

This particular service only depends on some Dll's from Entity Framework and Ninject. If you're curious as to how to build a Windows Service with Ninject. Check out that article from last week.

xUnit Test Build File

TeamCity has support for many testing frameworks but non natively for xUnit. However, we can install the xUnit runners on the build server and run a custom MSBUILD configuration against our visual studio project that contains all the tests.

First of all, I had to install the xUnit runners on the build server. In this instance, I put it with the TeamCity build agent in the tools subfolder: C:\TeamCity\buildAgent\tools\xunit_runner\xunit.runner.msbuild.dll. I took the xUnit runners and all its Dll's from my solution's packages folder.

Then at the top level of the solution I created a debug.msbuild file. I'll also have one for production or whatever I want to call it. My application is only in testing phase so we'll just call this one debug for now.

debug.msbuild:

<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">  
  <UsingTask AssemblyFile="C:\TeamCity\buildAgent\tools\xunit_runner\xunit.runner.msbuild.dll" TaskName="Xunit.Runner.MSBuild.xunit"/>
<Target Name="Build">  
    <MSBuild Projects="MyApplication.sln" Targets="Build" Properties="Configuration=Debug">
    </MSBuild>
    <xunit Assembly="MyApplication.Tests\bin\Debug\MyApplication.Tests.dll" />
</Target>  
</Project>  

Notice we called the Target: Build, and point to our xUnit runner. In this solution, the test project is called MyApplication.Tests and so we point to its dll file in the Debug folder. Of course this file won't exist yet but that's what it'll be once TeamCity builds it.

TeamCity Settings

Assuming the VCS has been setup in TeamCity, here's what the General Settings should look like:

TeamCity General Settings For Artifacts

Artifacts are final output files we want to keep such as msi installers. AppEventService.msi will be stored in a folder called installers

Next we'll want to restore all the nuget packages in the solution.

Restore Nuget packages in solution in TeamCity

Finally we will want to build our test project and run it. Remember that debug.msbuild file we made earlier? We'll have to point to it here in the build file path. We also named the target Build so we'll also specify that.
xunit TeamCity build settings

Give the build a run and we should see the tests run and the final msi file in the Artifacts dropdown.

comments powered by Disqus