Table of Contents
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.
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.
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.
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.
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.
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.
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:
="1.0" ="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" />
-->
<PropertyRef Id="NETFRAMEWORK40FULL" />
<Condition Message="This application requires .NET Framework 4.0 or greater.">
<![CDATA[]]>
</Condition>
-->
<Directory Id="TARGETDIR" Name="SourceDir">
-->
<Directory Id="ProgramFilesFolder">
-->
<Directory Id="AWESOME_DIR" Name="Awesome.App">
</Directory>
</Directory>
</Directory>
-->
<Feature Id="Complete" Title="Awesome Application"
Description="Paper Advice Engine Application."
Display="expand"
Level="1"
ConfigurableDirectory="AWESOME_DIR" >
-->
<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.
="1.0" ="utf-8"
<Project ToolsVersion="4.0" DefaultTargets="CreateInstaller"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-->
<PropertyGroup>
<Configuration>Release</Configuration>
<OutDir>bin\Installer\</OutDir>
<OutAppDir>$(OutDir)app\</OutAppDir>
<MsiOut>$(OutAppDir)Awesome.Setup.msi</MsiOut>
</PropertyGroup>
-->
<ItemGroup>
<WixCode Include="Installer.wxs" />
</ItemGroup>
-->
<ItemGroup>
<WixObject Include="Installer.wixobj" />
</ItemGroup>
<ItemGroup>
<Readme Include="README.TXT" />
</ItemGroup>
<PropertyGroup>
<InstallerDependsOn>
EmptyOutDir;
Build;
DBDeploy;
</InstallerDependsOn>
</PropertyGroup>
-->
<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>
-->
<Target Name="Build">
-->
<MSBuild
Projects="..\AwesomeApp.sln"
Targets="ReBuild"
Properties="Configuration=$(Configuration)" />
</Target>
<Target Name="CompileInstaller">
<Exec
Command='"$(WixPath)candle" -ext WiXNetFxExtension
@(WixCode, ' ')'
ContinueOnError="false"
WorkingDirectory="." />
<Exec
Command='"$(WixPath)light" -ext WiXNetFxExtension -out
$(MsiOut) @(WixObject, ' ')'
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
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:
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.
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.
Happy saving time while building your apps. :)
- 7 July 2011
- 23 July 2011
- Text and formatting revision
- Listing 4.1 and its description added