From MSI to WiX, Part 6 - Customizing installation using Custom Tables

The main page for the series is here.

 

Introduction

Say, we need to change an xml config file based on the environment our program will run in.  The most straightforward way of achieving that will be passing values which will go to the xml config file through public properties from the command line.

For this example I will be using C# console application.  Here is the content of Program.cs file:

using System;
using System.Collections.Generic;
using System.Text;
using System.Configuration;

namespace

ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
{
            Console.WriteLine("Key1={0}, Ke2={1}, Key3={2}",
                 ConfigurationManager.AppSettings["Key1"],
                 ConfigurationManager.AppSettings["Key2"],
                 ConfigurationManager.AppSettings["Key3"]);
}
}
}

and here is the App.config:

<?xml version="1.0" encoding="utf-8" ?>

<configuration>

<appSettings>

<add key="Key1" value="" />

<add key="Key2" value="" />

<add key="Key3" value="" />

</appSettings>

</configuration>

The Wix source is pretty simple in this case.  We are going to use <XmlFile> elements to update the config file.  We also will add launch conditions to make sure users passed the values.

Here is the source:

<?

xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="https://schemas.microsoft.com/wix/2003/01/wi">

<

Product Id="{1EFFDCD2-4B4B-439E-8296-651795EE02D9}"
                  Name="Minimal Windows Installer Sample"
                  Language="1033"
                  Codepage="1252"
                  Version="1.0.0"
                  Manufacturer="Acme Corporation"
                  UpgradeCode="{15F9543C-1C8D-45D6-B587-86E65F914F20}">

<

Package Id="{????????-????-????-????-????????????}"
                        Description="Minimal Windows Installer Sample"
                        Comments="This installer database contains the logic and data required to install Minimal Windows Installer Sample."
                        InstallerVersion="200"
                        Languages="1033"
                        SummaryCodepage="1252"
                        Platforms="Intel"
                        ReadOnly="no"
                        Compressed="yes"
                        AdminImage="no"
                        Keywords="Installer"
                        ShortNames ="no"
                        Manufacturer="Acme Corporation" />

<!--

Launch conditions -->
<Condition Message="KEY1 variable must be set in the command line">
            Installed OR KEY1
        </Condition>
<Condition Message="KEY2 variable must be set in the command line">
            Installed OR KEY2
        </Condition>
<Condition Message="KEY3 variable must be set in the command line">
            Installed OR KEY3
        </Condition>

<

Media Id="1" Cabinet="CAB001.cab" CompressionLevel="high" EmbedCab="yes" />

<

Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder">
<Directory Id="INSTALLDIR" Name="Minimal" LongName="MinimalInstallation">

<

Component Id="Component1"
                                       Guid="{A77C5B06-132D-4884-8E17-EA10A83C812D}">

<

File Id="ConsoleApp" DiskId="1" Name="ConsApp.exe" Source="ConsoleApp.exe" Vital="yes" KeyPath="yes" />

<

File Id="ConsoleApp.exe.config" DiskId="1" Name="ConsApp.exc" LongName="ConsoleApp.exe.config"
                                Vital="yes" Source="ConsoleApp.exe.config" />

<

XmlFile Id="SetKey1"
                                     Action="setValue"
                                     ElementPath="//appSettings/add[\[]@key='Key1'[\]]/@value"
                                     Value="[KEY1]"
                                     File="[INSTALLDIR]ConsoleApp.exe.config" />
<XmlFile Id="SetKey2"
                                     Action="setValue"
                                     ElementPath="//appSettings/add[\[]@key='Key2'[\]]/@value"
                                     Value="[KEY2]"
                                     File="[INSTALLDIR]ConsoleApp.exe.config" />
<XmlFile Id="SetKey3"
                                     Action="setValue"
                                     ElementPath="//appSettings/add[\[]@key='Key3'[\]]/@value"
                                     Value="[KEY3]"
                                     File="[INSTALLDIR]ConsoleApp.exe.config" />

</

Component>

</

Directory>
</Directory>
</Directory>

<

Feature Id="Feature1"
                      Title="Feature1 title"
                      Description="Feature1 description"
                      Level="1"
                      ConfigurableDirectory="INSTALLDIR" >
<ComponentRef Id="Component1" />
</Feature>

</

Product>
</Wix>

Here are commands to build the msi:

candle.exe Minimal.wxs
light.exe -out Minimal.msi Minimal.wixobj d:\wix\wixca.wixlib

or if you prefer MSBuild:

<

Project DefaultTargets="Build" xmlns="https://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<!-- Required by WiX -->
<!-- Path and name of the output without extension -->
<OutputName>Minimal</OutputName>

<!--

What need to be built -->
<OutputType Condition="$(OutputType)==''">package</OutputType>

<!--

The path to the WiX installation -->
<ToolPath>d:\WIX\</ToolPath>

<!--

Input path to source files.
If not passed, assumes the same folder where project file is located. -->
<BaseInputPath Condition="$(BaseInputPath)==''">$(MSBuildProjectDirectory)\</BaseInputPath>

<!--

Create a compiled output in the folder where project is located -->
<OutputPath Condition="$(OutputPath)==''">$(MSBuildProjectDirectory)\</OutputPath>

<!--

Add missing trailing slash in paths -->
<ToolPath Condition="!HasTrailingSlash('$(ToolPath)') ">$(ToolPath)\</ToolPath>
<BaseInputPath Condition="!HasTrailingSlash('$(BaseInputPath)') ">$(BaseInputPath)\</BaseInputPath>
<OutputPath Condition="!HasTrailingSlash('$(OutputPath)') ">$(OutputPath)\</OutputPath>
</PropertyGroup>

<!--

Candle.exe command-line options -->
<ItemGroup>
</ItemGroup>

<!--

Light.exe command-line options -->
<ItemGroup>
<WixLibrary Include="$(ToolPath)wixca.wixlib"></WixLibrary>
</ItemGroup>

<

Import Project="$(ToolPath)wix.targets"/>

<!--

List of files to compile -->
<ItemGroup>
<Compile Include="$(BaseInputPath)Minimal.wxs"/>
</ItemGroup>

</

Project>

While this solution certainly works it has lots of drawbacks.  Just to name few - it has problem with maintenace mode (who will provide those values in the command line?) and it creates support issues when amount of parameters will increase.

So, to address those issues we can try another solution which is based on using Custom Tables to store the configuration information.

MSI allows us to create custom tables in order to allow developers create data-driven installation.  In Wix we are using <CustomTable> and <Column> elements to describe the layout of the custom table.  To add the data to the custom table we are using <Row> and <Data> elements.

Insert this description of our custom table after <Media> element in our Wix source file:

<CustomTable Id="EnvironmentSettings">
<Column Id="Id" Category="Identifier" PrimaryKey="yes" Type="int" Width="4" />
<Column Id="Environment" Category="Text" Type="string" PrimaryKey="no" />
<Column Id="Key" Category="Text" Type="string" PrimaryKey="no" />
<Column Id="Value" Category="Text" Type="string" PrimaryKey="no" />
<Row>
<Data Column="Id">1</Data>
<Data Column="Environment">Dev</Data>
<Data Column="Key">KEY1</Data>
<Data Column="Value">Dev1</Data>
</Row>
<Row>
<Data Column="Id">2</Data>
<Data Column="Environment">Dev</Data>
<Data Column="Key">KEY2</Data>
<Data Column="Value">Dev2</Data>
</Row>
<Row>
<Data Column="Id">3</Data>
<Data Column="Environment">Dev</Data>
<Data Column="Key">KEY3</Data>
<Data Column="Value">Dev3</Data>
</Row>
<Row>
<Data Column="Id">4</Data>
<Data Column="Environment">Test</Data>
<Data Column="Key">KEY1</Data>
<Data Column="Value">Test1</Data>
</Row>
<Row>
<Data Column="Id">5</Data>
<Data Column="Environment">Test</Data>
<Data Column="Key">KEY2</Data>
<Data Column="Value">Test2</Data>
</Row>
<Row>
<Data Column="Id">6</Data>
<Data Column="Environment">Test</Data>
<Data Column="Key">KEY3</Data>
<Data Column="Value">Test3</Data>
</Row>
</CustomTable>

Id attribute of the <CustomTable> element defines that the name of our custom table in the installation database will be EnvironmentSettings.  Our table will have four columns: Id, Environment, Key, and Value.  At least one column must be a primary key and we will be using Id column as a primary key. Environment column will group our Key/Value pairs in the different sets of customization data.

Now, instead of passing Key1, Key2, and Key3 in the command line we need to pass just one parameter - ENVIRONMENT, which will be used in order to determine which set of data to use during installation to update the content of the configuration file.  Because of this, we need to remove launch conditions from previous example and use these instead:

<!--

Launch conditions -->
<Condition Message="ENVIRONMENT variable must be set in the command line">
    Installed OR ENVIRONMENT
</Condition>

Now, when we have data, we need also custom action to set the values of KEY1, KEY2, and KEY3 properties depending on the value passed in the ENVIRONMENT public property.  For simplicity sake we will be using VBScript custom action:

<

CustomAction Id="SetProperties" BinaryKey="SPScript" VBScriptCall="SetProperties" Execute="immediate" />

<

Binary Id="SPScript" SourceFile="SPScript.vbs"/>

Here we store SPScript.vbs internally in installation database as embedded stream and use it as immediate custom action.  We also need to schedule this custom action.  Because script may fail to set the properties, for example, because of wrong value passed in ENVIRONMENT property in the command line, we want to have a launch condition on this as well.  So, we need to schedule it before LaunchConditions and AppSearch actions.  In this example I just use sequence number 1:

<InstallExecuteSequence>

<Custom Action="SetProperties" Sequence="1">Not Installed</Custom>

</InstallExecuteSequence>

<InstallUISequence>

<Custom Action="SetProperties" Sequence="1">Not Installed</Custom>

</InstallUISequence>

And here is the additional launch condition to make sure that custom action successfully set the properties with the values from the custom table:

<!-- In case script will fail -->
<Condition Message="Script has failed to set up the properties">
    Installed OR (KEY1 AND KEY2 AND KEY3)
</Condition>

Here is the content of the VBScript custom action file SPScript.vbs:

Function

SetProperties()
    Dim Environment
    Dim Database
    Dim View
    Dim Record
    Dim msiDoActionStatusSuccess : msiDoActionStatusSuccess = 1
    Dim msiDoActionStatusFailure : msiDoActionStatusFailure = 3

    On Error Resume Next

Environment = Session.Property(

"ENVIRONMENT")

    Set Database = Session.Database
    If Database Is Nothing Then
        MsgBox "Database is nothing: " & Err.Description
SetProperties = msiDoActionStatusFailure
        Exit Function
    End If

    Set View = Database.OpenView("SELECT `Key`, `Value` FROM `EnvironmentSettings` WHERE `Environment` = " & Chr(39) & Environment & Chr(39))
    If View Is Nothing Then
        MsgBox "View is nothing: " & Err.Description
        Set Database = Nothing
        SetProperties = msiDoActionStatusFailure
        Exit Function
    End If

View.Execute

    Set Record = View.Fetch

    Do Until Record Is Nothing
        Session.Property(Record.StringData(1)) = Record.StringData(2)
        Set Record = View.Fetch
    Loop

View.Close

    Set View = Nothing
    Set Database = Nothing

    ' return success
    SetProperties = msiDoActionStatusSuccess
    Exit Function

End

Function

Script is pretty straightforward.  You can find more information on Windows Installer automation here.