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=http://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=http://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.


 

Comments (5)

  1. Aleksey Timohin says:

    1) Following seems not clear to me:

    > 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.

    What value should be defined for ENVIRONMENT to work?

    msiexec Setup.msi ENVIRONMENT = ????

    or maybe it will be enough just specify this property?

    msiexec Setup.msi ENVIRONMENT

    2) Can Custom Tables be used to store values that user has enetered during install?

    p.s. thank you for great articles!

  2. ENVIRONMENT variable should be set to one of the values in Environment column of custom table, ie. Dev or Test:

    msiexec /i Setup.msi ENVIRONMENT=Dev

    Answer for second question is No.  It is possible to change values in custom table during install, but these changes won’t be saved in actual msi.  To store values entered during install, usually these values persisted in the registry during fresh install and retrieved from registry during maintenance/uninstall.

  3. Aleksey Timohin says:

    Thank you for fast answer.

    Custom Tables became clearer for me.

  4. Paul says:

    For the first solution you said:

    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.

    But as far as I understand, in the second solution you *also* need to provide a value in the command line, right? So who will provide that value?

    Thanks,

    Paul

  5. Hi Paul.  I see your point.  I wrote this over two years ago and don’t remember what was my intent, but obviously this sentence is wrong.  What it should say that instead of saving a bunch of properties, possibly in registry, we need to save just one.  More information on saving property values in http://blogs.technet.com/alexshev/archive/2009/05/13/preserving-properties-used-during-install.aspx.

    Thanks for bringing this to my attention, hopefully soon I will refresh most of the content I have so far.  It is obviously obsolete.