From the previous post we have these custom targets we want to create; MSI package building, Database deploy, Service installation, and Web deploy. I will go through each of them and give you some hints on how the targets were created.

MSI package building

For this part we need to know the solution name, the project containing the setup and where VisualStudio is installed. Remember the registry thing from the last post? Here it really comes in handy.

<PropertyGroup>
  <VS>$(registry:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\
    VisualStudio\9.0\@InstallDir)\devenv</VS>
</PropertyGroup>

<Target Name="BuildMsi">
  <Message Text="Building package $(Vdproj)" />
  <Exec Command='"$(VS)" "$(Sln)" /Build "Release|Any CPU"
    /project "$(Vdproj)" 
    /ProjectConfig "Release"'/>
</Target>

Never mind the extra line breaks, it’s for readability. But note the single quote that is used on the Command part, this makes it possible to use double quote in the command text for readability. The other option is to use &quote; but that really makes it very hard to read the scripts.

Database deploy

When we deploy a database you typically have five parameters involved; where, who and what. Oh, the who part is user and password and where includes both server and database, a total of five. Now, let us start with a simple target and a magic database task.

<Target Name="DeployDatabase">
  <Message Text="Deploying $(DBContent) to $(DB) on $(Server)" />
  <MagicTask DataPath="$(DBContent)"
    ConnectionString="Database=$(DB);Server=$(Server);
UID=$(User);PWD=$(Pass)"
DropDatabase="true" /> <Message Text="Database deployed on $(Server)" /> </Target>

The MagicTask is depending on how you deploy the database, we are using NHibernate and have written custom tasks to handle the deployment. There are a set of build tasks on CodePlex, created by a college of mine, that can help you with the specific tasks for database deployment.

Service installation

When it comes to service installation you have to get hold of a little bag of tricks to get all of it working. Locally there is not as big issue . Just call the MSI installer and execute it silently both for uninstall and install.

<Exec Command='msiexec /i "$(Msi)" /qn' />

$(Msi) holds the full path of the installer package. When it’s installed, it should be started and for that we use net start with the service name as parameter.

<Exec Command="net start $(Service)" />

But how do you do it remotely? Here’s where the bag of tricks come in handy. First you need a place on the remote system where you can transfer the files. Make sure the agent has access right to copy the files there. Then, use some magic from Sysinternals, named PsExec.exe. This little tool makes it possible to remotely execute net start and msiexec. his target we need the MSI, the Server, user and password and the name of the Service that is installed. You need to touch up the scripts with some of your own paths to make sure you have all the files in the right place to start with.

<PropertyGroup>
  <!-- The sysinternals tool -->
  <PsExec>$(MSBuildStartupDirectory)\Tools\psexec.exe</PsExec>
  <!-- Here is the new MSI package is placed -->
  <NewMsi>$(MSBuildStartupDirectory)\msi</NewMsi>
  <!-- Here is the currently installed MSI package placed -->
  <SharedMsi>$(server)\msi</SharedMsi>
</PropertyGroup>

<Target Name="InstallMsi">
  <Message Text="Installing and starting $(msi)/>

  <!-- First Uninstall the old service if the config is there -->
  <Exec Command='$(PsExec) -accepteula $(Server) 
        -u $(User) -p $(Pass) -w "$(OldMsi)" -n 60 
        msiexec /x "$(SharedMsi)\$(msi)" /quiet' />

  <!-- Copy and install the new service -->
  <Copy SourceFiles="$(NewMsi)\$(msi)" 
    DestinationFolder="$(OldMsi)" />
  <Exec Command='$(PsExec) -accepteula $(Server) 
        -u $(User) -p $(Pass) msiexec /i 
        "$(SharedMsi)\$(msi)" /qn' />

  <!-- and start the service -->
  <Exec Command='$(PsExec) $(Server) -u 
        $(User) -p $(Pass) -d net start $(Service)' />

  <Message Text="$(Service) installed and started" />
 </Target>

Hopefully this will give you the basic idea. For all the switches on PsExec, look into Sysinternals. But one that can be a real timeserver (often hangs the build) is the –accepteula switch, it auto accepts the EULA splash screen.

Web deploy

Maybe the simplest deployment is the web deployment. Most of the time there is nothing more to it than copy the files and you are up and running. However, you may need to set some configuration etc depending on how your environments are setup. But for now a simple target like this will do the trick for web deployment:

<ItemGroup>
  <WebContent Include="$(ContentPath)\**\*.*" />
</ItemGroup>
  
<Target Name="DeployWeb">
  <Message Text="Installing web on $(Server)" />
  <Copy SourceFiles="@(WebContent)" 
    DestinationFolder="$(Server)\$(WebFolder)\%(RecursiveDir)" />
</Target>

The ItemGroup here will produce a recursive list of files and folders from the ContentPath and it get placed in WebContent item group. The files will then be copied to the WebFolder on the Server. So here we have three properties to set for this target. Also make sure you have the correct access levels on the folders and you can do this remote as well.

Calling the targets

Now that we have the targets in place, how do we get all the properties into them? Well, custom targets can be accessed in a couple of different ways. You can import them and then call them as usual and all the properties you set in the calling script are set as if the target was in your current script.

<Import Project="CustomTargets.proj" />

<PropertyGroup>
  <Server>Srv01</Server>
  <User>Kalle</User>
  <Pass>Kula</Pass>
  <Msi>setup.msi</Msi>
</PropertyGroup>

<Target Name="default">
  <CallTarget Targets="InstallMsi" />
</Target>

But in this case I want to pass in more than one set of properties to the target to make it more flexible. And in that case it’s easier to use the MSBuild task. This will execute the target as if you were calling it from command line.

<MSBuild Projects="CustomTargets.proj" 
  Targets="DeployDatabase"
  Properties="Server=DbSrv01;User=sa;Pass=sa;DB=Base;DBContent=Data" />

All together now

All this is, in our case, trigged from TeamCity, but could of course be triggered from other build systems as well. Adding some initial parameters, like where to deploy things, makes it very easy for me to add more servers later on. For now I just execute MSBuild with the needed properties.

MSBuild deploy.msbuild /property:DB=DbSrv01;DeployOn=Srv01;Web=WwwRoot

You have to make a lot of choices where to trigger and store the properties to maximize flexibility and readability. Additionally, some smart naming of files and projects will help in minimizing the number of properties. All this is of course depending on the project you are deploying.

And one more thing, make sure the build agents are executing with the right access rights, or you will not be able to execute some of the commands. This is especially important when it comes to accessing the other systems through PsExec. It took me a while to fix that problem :)

// Håkan Reis