Click here to Skip to main content
Click here to Skip to main content

Delivery Process of Application to Windows Platform – Simple, Reliable and Repeatable

By , 22 Jul 2011
 
intro.jpg

Table of Contents

Introduction

The intention of this article is to provide one option to deliver software targeting Windows platform. The install approach in this article is command line based which means it can be used in Continuous Delivery practice. You will learn how to install/uninstall application to different environments and how to manage the configuration during the installation because every environment requires a unique configuration.

1.jpg

Background

The delivery process is not so easy. I guess everyone knows the stage when you finish writing an application and you try to deliver it. Depending on the target environment, it starts to crash on different issues like non existing E drive, wrong connection string, wrong addresses to APIs and so on. After a while, you build up some mechanism that allows you to manually specify the values during the installation. Like installation UI Wizards. Then you end up spending a lot of time to fill up the Wizards again and again, installation after installation.

Now it comes to automate to process. Make it easy, simple, reliable and repeatable. And this process I’m going describe now.

What Kind of Software Do We Deliver?

Could be any software targeting Windows Platform. In this article, I decided to use a simple console application with a simple database as described in the two following chapters. I assume, in most cases, it is needed to deploy an application and the database. The practice used in this article works fine no matter how complicated the installer is. It will definitely work fine for example for the installer from this article.

Preparing Executables

For demonstration in this article, I choose a simple “Hello world” application called AwesomeApp. The application consists of a single file Program.cs which possesses the following lines of code:

using System;
namespace AwesomeApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Awesome Hello World!");
        }
    }
}
Listing 1 - Program.cs

The application has two configuration modes: Debug and Release. For the deployment is used the Release mode of configuration which means the following line of code to compile the solution:

msbuild /t:ReBuild /p:Configuration=Release AwesomeApp.sln

As you can see, the first part the executable part of delivery package is straightforward and easy to compile. The second part, however, could be a bit more interesting because it is related to DB.

Preparing DB Scripts

Regarding the database deployment, I decided to use the Database Management Studio which is included in Visual Studio 2010. So I created a database project with the name Awesome.Database. There is a single table in it called AwesomeTable.

vs.jpg

The database project has also just two configuration modes: Debug and Release. For deployment here is also used the Release mode. The command to compile the project and get the script for creating database is as follows:

msbuild /t:DBDeploy /p:Configuration=Release Awesome.Database.dbproj

However, creating of the DB script does not relay just on the previous command. We need to configure more than that to achieve demanded result. If you open the .dbproj file in your favorite text editor, you may see the PropertyGroup element of configuration for Release mode.

<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
    <OutputPath>.\sql\release\</OutputPath>
    <BuildScriptName>$(MSBuildProjectName).sql</BuildScriptName>
    <TargetConnectionString>
    </TargetConnectionString>
    <TargetDatabase>
    </TargetDatabase>
    <TreatWarningsAsErrors>False</TreatWarningsAsErrors>
    <SuppressWarnings>
    </SuppressWarnings>
    <DeploymentConfigFile>Properties\Database.sqldeployment</DeploymentConfigFile>
    <SqlCommandVariablesFile>Properties\Database.sqlcmdvars</SqlCommandVariablesFile>
    <DeployToDatabase>False</DeployToDatabase>
  </PropertyGroup>

As you can see on the previous Listing, there are several variables you can set up. For example, TargetConnectionString and TargetDatabase if you wish to create the script according to the existing database or SqlCommandVariablesFile to set up default values for variables in your SQL scripts. The most important value in this part is DeployToDatabase which means that the deployment script will be created but the deployment itself will not be performed. Usually we have several configuration modes because we deploy a database to several servers. I recommend you to create a special configuration for your deployment package. However, in this article, I stick with Release configuration.

Writing an Installer

In the two previous chapters, we prepared an executable file and a SQL script file. Those two files are the application to deploy. So let’s write a simple installer for that. The installer can look as follows:

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
	 xmlns:util="http://schemas.microsoft.com/wix/UtilExtension"
	 xmlns:netfx="http://schemas.microsoft.com/wix/NetFxExtension">

  <Product Id="{DACD6B11-B50F-43B0-9FCB-46779FDC7AA3}"
           Name="Awesome.App"
           Language="1033"
           Version="1.0.0.0"
           Manufacturer="My"
           UpgradeCode="{99B33695-E79A-4957-8C70-DAB6B2CD1C7A}">

    <Package InstallerVersion="200" Compressed="yes" />
    <Property Id="ALLUSERS" Value="1" />
    <Media Id="1" Cabinet="media1.cab" EmbedCab="yes" />

    <!-- Check for .NET 4.0 -->
    <PropertyRef Id="NETFRAMEWORK40FULL" />
    <Condition Message="This application requires .NET Framework 4.0 or greater.">
      <![CDATA[Installed OR (NETFRAMEWORK40FULL AND NETFRAMEWORK40FULL = "#1")]]>
    </Condition>

    <!-- The primary directory structure -->
    <Directory Id="TARGETDIR" Name="SourceDir">
      <!-- The install folder. -->
      <Directory Id="ProgramFilesFolder">
        <!-- The specific folder for this product -->
        <Directory Id="AWESOME_DIR" Name="Awesome.App">
        </Directory>
      </Directory>
    </Directory>

    <!-- Defining features. -->
    <Feature Id="Complete" Title="Awesome Application"
				 Description="Paper Advice Engine Application."
				 Display="expand"
				 Level="1"
				 ConfigurableDirectory="AWESOME_DIR" >
      <!-- Specify the component which belongs to this feature. -->
      <ComponentRef Id="AWESOME_CMP" />
    </Feature>

    <DirectoryRef Id="AWESOME_DIR">
      <Component Id="AWESOME_CMP" Guid="{96E75B26-530E-4F4D-A95F-14385BF85511}">
        <File Id="Exe_File" Source=".\bin\Release\AwesomeApp.exe" 
		KeyPath="yes" Vital="yes" />
        <File Id="Sql_File" 
	Source="..\Awesome.Database\sql\Release\Awesome.Database.sql" Vital="yes" />
      </Component>
    </DirectoryRef>

  </Product>
</Wix>
Listing 3 - Installer.wsx

The installer is really very simple. As you can see at the bottom of the file, there is the component with two files: AwesomeApp.exe and Awesome.Dabase.sql. Those files are copied to the target installation folder and that is the only thing the installer does.

If you have the WIX 3.5 installed on your computer, you can create the installer by the following commands:

candle  -ext WiXNetFxExtension Installer.wxs
light  -ext WiXNetFxExtension –out Awesome.Setup.msi  Installer.wixobj
Listing 4 - Compile Installer

Of course, it is not comfortable to build the installer using commands from Listing 4. Those who read some of my other articles related to WIX know that I am about to introduce the msbuild script which takes care of the whole process of creating installer. The script could be seen in the Listing 4.1.

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="CreateInstaller" 
	xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <!-- 
		Build related.
	-->
  <PropertyGroup>
    <Configuration>Release</Configuration>
    <OutDir>bin\Installer\</OutDir>
    <OutAppDir>$(OutDir)app\</OutAppDir>
    <MsiOut>$(OutAppDir)Awesome.Setup.msi</MsiOut>
  </PropertyGroup>

  <!-- The list of WIX input files -->
  <ItemGroup>
    <WixCode Include="Installer.wxs" />
  </ItemGroup>

  <!-- The list of WIX after candle files -->
  <ItemGroup>
    <WixObject Include="Installer.wixobj" />
  </ItemGroup>

  <ItemGroup>
    <Readme Include="README.TXT" />
  </ItemGroup>

  <PropertyGroup>
    <InstallerDependsOn>
      EmptyOutDir;
      Build;
      DBDeploy;
    </InstallerDependsOn>
  </PropertyGroup>


  <!-- Main target -->
  <Target Name="CreateInstaller" DependsOnTargets="$(InstallerDependsOn)">
    <CallTarget Targets="CompileInstaller" />
    <Copy SourceFiles="install.bat" DestinationFolder="$(OutAppDir)" 
	ContinueOnError="false" />
    <Copy SourceFiles="cnf_server1.bat" DestinationFolder="$(OutAppDir)" 
	ContinueOnError="false" />
    <Copy SourceFiles="cnf_server2.bat" DestinationFolder="$(OutAppDir)" 
	ContinueOnError="false" />
    <Delete Files="$(OutAppDir)Awesome.Setup.wixpdb" ContinueOnError="false" />
  </Target>


  <!-- Define default target with name 'Build' -->
  <Target Name="Build">
    <!-- Compile whole solution in release mode -->
    <MSBuild
			Projects="..\AwesomeApp.sln"
			Targets="ReBuild"
			Properties="Configuration=$(Configuration)" />
  </Target>

  <Target Name="CompileInstaller">
    <Exec
			Command='"$(WixPath)candle" -ext WiXNetFxExtension 
						@(WixCode, &apos; &apos;)'
			ContinueOnError="false"
			WorkingDirectory="." />
    <Exec
			Command='"$(WixPath)light" -ext WiXNetFxExtension -out 
					$(MsiOut) @(WixObject, &apos; &apos;)'
			ContinueOnError="false"
			WorkingDirectory="." />
    <CallTarget Targets="CopyDoc" />
  </Target>

  <Target Name="DBDeploy">
    <MSBuild Projects="..\Awesome.Database\Awesome.Database.dbproj" 
	Properties="Configuration=$(Configuration)" Targets="DBDeploy" />
  </Target>

  <Target Name="CopyDoc">
    <MakeDir Directories="$(OutDocDir)" ContinueOnError="true" />
    <Copy SourceFiles="@(Readme)" 
	DestinationFiles="@(Readme->'$(OutDir)%(Filename)%(Extension)')" 
	ContinueOnError="false" />
  </Target>

  <Target Name="EmptyOutDir">
    <ItemGroup>
      <FilesToDelete Include="$(OutDir)**\*.*" />
    </ItemGroup>
    <Delete Files="@(FilesToDelete)" ContinueOnError="false" />
  </Target>

</Project>
Listing 4.1 - Installer.build

The default target in the script is the CreateInstaller target. This target depends on three other targets. Those have to be executed in advance. First target EmptyOutDir is cleaning the output folder. The second target Build is building the solution file in release mode. The third target DBDeploy is creating a database deployment script. But not deploying anything. Inside of the CreateInstaller target is executed CompileInstaller target which finally contains the commands calling candle.exe and light.exe from WIX.

To put a summary on that, you can simply create an installer by the following command:

msbuild installer.build

Configuring Delivery Process

At last, here comes the interesting part of delivery process, it is configuration. Since we have the installer package prepared, we can wrap it into a bat file which configures the installer for proper environment before executing. This can be seen in the following fixture:

2.jpg

The installer.bat file keeps the same code for all installation. You can decide how complicated it will be and how much it will be doing. In this example, I wrote a bat file which installs/uninstalls the application and installs database if needed. The code of such functionality could be seen in the following listing:

@ECHO OFF
@echo
@echo ================================================================

IF [%1]==[/x] GOTO UNINSTALL
IF [%1]==[/?] GOTO HELP
IF NOT EXIST "%~f1" GOTO CNF_MISSING

:SET_VARIABLES
call "%~f1"

@echo Testing of sqlcmd.exe availability 
%SQLCMD% /?
IF NOT %ERRORLEVEL% EQU 0 GOTO CMD_SQL_NOT_FOUND
cls
@echo
@echo ================================================================
@echo Configuring installation based on %~f1
SET LOG_FILE=Awesome.App.log.txt

:APP_INSTALL
@echo Awesome Application by me
@echo Installing Awesome app...

msiexec /q /i Awesome.Setup.msi AWESOME_DIR="%AWESOME_DIR%" /log %LOG_FILE%

IF NOT %ERRORLEVEL% EQU 0 GOTO APP_ERROR
@echo Awesome app installation succeeded

:DB_INSTALL
IF [%2]==[/skipDb] GOTO END

@echo Installing DB on server %SQLSERVER%...
%SQLCMD% -S %SQLSERVER% -E -i "%AWESOME_DIR%\Awesome.Database.sql" -f 65001

IF NOT %ERRORLEVEL% EQU 0 GOTO DB_ERROR
@echo DB installed

GOTO END

:UNINSTALL
@echo Uninstalling Awesome app...
msiexec /q /x {DACD6B11-B50F-43B0-9FCB-46779FDC7AA3}
@echo Uninstall finished.

GOTO END

:HELP
@echo Install Script for Awesome app
@echo   Examples:
@echo     install.bat cnf_server1.bat             - installs the according to 
						cnf_server1.bat configuration
@echo     install.bat cnf_server1.bat /skipDb     - installs application without DB.
@echo     install.bat /x                          - uninstalls the application 
						but not database.
@echo     install.bat /?                          - help

GOTO END

:APP_ERROR
@echo Installation of Awesome application failed. 
	See the log for more information: %LOG_FILE%
GOTO HELP

:DB_ERROR
@echo Creating Database failed. 
GOTO UNINSTALL

:CMD_SQL_NOT_FOUND
@echo %SQLCMD% was not found. Please set up the variable to 
	point out to this utility before executing.
GOTO HELP

:CNF_MISSING
@echo Configuration file is missing. Please specify configuration file as first parameter
GOTO HELP

:END

@echo ================================================================
@echo
Listing 5 – Install.bat

I hope the code is not so complicated. If you run it with the /? option, you get the help. What I want to point out now is the code:

:SET_VARIABLES
call "%~f1"

which is calling the configuration script. The configuration script is passed as the first parameter to the install.bat and it sets up the variables which are passed to the installer later on. Exactly in this part:

msiexec /q /i Awesome.Setup.msi AWESOME_DIR="%AWESOME_DIR%" /log %LOG_FILE%
Listing 6

As you probably noticed, the AWESOME_DIR variable is not defined in the install.bat script. If you open one of the configuration files, you will see something that is listed in the following Listing:

REM -------------------------------------------------------------------------------
REM
REM Please set up the variables in this file
REM The value of the variable follows after = 
	sign without any additional characters like "
REM even if you have a space in your value, do NOT use " to surround it.
REM -------------------------------------------------------------------------------

REM -----------------------------------------------
REM Target directory for installation. The Path where the application will be deployed.
SET AWESOME_DIR=C:\my_apps\awesome

REM -----------------------------------------------
REM DB server for database deploy
SET SQLSERVER=localhost

REM ------------------------------------------------
REM Continue with other variables like connection string and etc.

REM -----------------------------------------------
REM Path to sqlcmd.exe utility if necessary.
REM If the MSSQL utilities are installed correctly then 
	this do not need to be changed otherwise please set up the right path.
REM The installation will automatically check if the sqlcmd.exe is available. 
REM If not then it will not apply any changes to the target system.
SET SQLCMD=sqlcmd.exe
Listing 7

There are many aspects for configuration depending on the server. Target directory, connection string, registry values… Simply to say: a single configuration script for a single environment.

Remarks

You probably noticed that the uninstall process is not part of the introduced script. Well, open question.

You also can say that this approach is useful for delivering to servers but not for delivering to workstation and you require the UI during the installation. Like wizards for gathering variables from user during the installation. Well, that is simply not true. You can always build your installer with huge amount of UI screens and for the Continuous Delivery process, just keep it quiet using /q option in msiexec application.

Conclusion

Happy saving time while building your apps. :)

History

  • 7 July 2011
    • Initial release
  • 23 July 2011
    • Text and formatting revision
    • Listing 4.1 and its description added

License

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

About the Author

Petr Pechovic
Technical Lead
Czech Republic Czech Republic

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
Hint: For improved responsiveness ensure Javascript is enabled and choose 'Normal' from the Layout dropdown and hit 'Update'.
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 5membermanoj kumar choubey3-Oct-12 22:21 
GeneralMy vote of 5membernsraja18-Jul-12 22:17 
GeneralRe: My vote of 5memberPetr Pechovic16-Aug-12 21:03 
QuestionGood for simple apps?memberSkif1-Jul-11 12:07 
AnswerRe: Good for simple apps?memberPetr Pechovic1-Jul-11 21:45 
Hi Skif,
 
thank you for your interest and thank you for your comment.
 
Yes, this will work also for complicated installer. See the chapter What kind of software do we deliver[^]
 
I'm mostly using it in complicated installers but this article is about the approach and thus the installer is "Hello world".
 
- dozens of files have to be updated/deleted/added;
Done by installer (msi). Run uninstall and install again.
 
- the database has to be updated with existing data preserved;
I prefere delete database and deploy again with "Insert Into", however, this could not be done in production-like environment. Well, this is complicated topic and could be written in stanalone article. You can for example create a diff script using Database studio and deploy that diff script but then again Database studio can't create complicated diff script without human interaction... Well, as I said I prefere complete redeploy of the database but it's up to lead developer.
 
- configuration files have to be updated based on selected/existing configuration;
Done by installer (msi)
 
- the deployment script has to properly work on different old versions, etc.
I see no problem in this script to work across different versions. This will work across different products the only thing you need to change in install.bat is the help statement and parameters in "msiexec" call.
 

Have a nice weekend,
Petr
GeneralRe: Good for simple apps?memberdave.dolan2-Jul-11 19:35 
GeneralRe: Good for simple apps?memberPetr Pechovic3-Jul-11 22:14 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130619.1 | Last Updated 23 Jul 2011
Article Copyright 2011 by Petr Pechovic
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid