From MSI to WiX, Part 7 - Customizing installation using Transforms

The main page for the series is here.

 

Introduction

In previous blog we were using custom tables to store the data for different environments.  Now, we will be using transforms to customize installation.  In order to create a Transform we need a base installation database and extended installation database.  Transform is the difference between base and extended installation databases.

Creating Base Installation Database

First, we need to create a Base installation which does not any customization data.  Here is modified source Wix file from previous blog:

<?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" />

<

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 Minimal.msi:

candle.exe Minimal.wxs
light.exe -out Minimal.msi Minimal.wixobj

Creating extended database using Orca

Create a copy of Minimal.msi and rename it to MinimalTest.msi.  This will be our extended installation database for the test environment.  Open MinimalTest.msi using Orca, click on Property table in the Tables window on the left.  Add three new properties to the Property table:

Property Value
KEY1 Test1
KEY2 Test2
KEY3 Test3

Save the installation database.

Now we need to generate a transform which will have just the difference between bae and extended database.  To do that, we need to run this command (MsiTran.exe will be installed on your machine if you installed the Platform SDK/Windows Installer SDK):

MsiTran -g Minimal.msi MinimalTest.msi Test.mst

To install the product use this command:

msiexec /i Minimal.msi TRANSFORMS=Test.mst

Same procedure should be executed for any additional environment.

Automating the process of creating transforms

We can use Windows Installer Automation Interface to automate the process of cretaing transforms.  Here is the script which creates both Test.mst and Dev.mst:

Option Explicit

Const msiOpenDatabaseModeReadOnly = 0

Const msiOpenDatabaseModeTransact = 1

Const msiViewModifyAssign = 3

Const msiTransformErrorNone = 0

Const msiTransformValidationNone = 0

On Error Resume Next

Dim installer : Set installer = Nothing

Dim Devs

Set Devs = CreateObject("Scripting.Dictionary")

Devs.Add "KEY1", "Dev1"

Devs.Add "KEY2", "Dev2"

Devs.Add "KEY3", "Dev3"

CreateTransform "Dev", Devs

Dim Test

Set Test = CreateObject("Scripting.Dictionary")

Test.Add "KEY1", "Test1"

Test.Add "KEY2", "Test2"

Test.Add "KEY3", "Test3"

CreateTransform "Test", Test

Wscript.Quit 0

Sub CreateTransform(TransformName, Values)

    ' Create a copy of base database

    Dim fso

    Set fso = CreateObject("Scripting.FileSystemObject") : CheckError

    fso.CopyFile "Minimal.msi", TransformName & ".msi" : CheckError

    Set fso = Nothing

    ' Add additional properties to the copy

    Dim database

    Dim view

    Dim record

    Set installer = Wscript.CreateObject("WindowsInstaller.Installer") : CheckError

    Set database = installer.OpenDatabase(TransformName & ".msi", msiOpenDatabaseModeTransact) : CheckError

    Set view = database.OpenView("SELECT `Property`,`Value` FROM Property") : CheckError

    view.Execute : CheckError

    Dim keys : keys = Values.Keys

    dim items : items = Values.Items

    Dim i

    For i = 0 To Values.Count - 1

        Set record = installer.CreateRecord(2)

        record.StringData(1) = keys(i)

        record.StringData(2) = items(i)

        view.Modify msiViewModifyAssign, record : CheckError

    Next

    view.Close

    Set view = Nothing

    database.Commit : CheckError

    Set database = Nothing

   

    ' Create a Transform

    Dim database1 : Set database1 = installer.OpenDatabase("Minimal.msi", msiOpenDatabaseModeReadOnly) : CheckError

    Dim database2 : Set database2 = installer.OpenDatabase(TransformName & ".msi", msiOpenDatabaseModeReadOnly) : CheckError

    Dim transform : transform = TransformName & ".mst"

    database2.GenerateTransform database1, transform : CheckError

    database2.CreateTransformSummaryInfo database1, transform, msiTransformErrorNone, msiTransformValidationNone : CheckError

    Set database1 = Nothing

    Set database2 = Nothing

    Set installer = Nothing

End Sub

Sub CheckError

    If Err = 0 Then Exit Sub

    Dim message, errRec

    message = Err.Source & " " & Hex(Err) & ": " & Err.Description

    If Not installer Is Nothing Then

        Set errRec = installer.LastErrorRecord

        If Not errRec Is Nothing Then message = message & vbNewLine & errRec.FormatText

    End If

    Wscript.Echo message

    Wscript.Quit 2

End Sub

Embedding Transforms into installer database

Instead of shipping installer database and a separate transform file to a customer we can embed the Transform to an installer database and ship just one file.  In fact, we can embed multiple transforms in the database.  Here is the fragment of updated version of Transforms generating script from previous section:

' Create a Transform

Dim database1 : Set database1 = installer.OpenDatabase("Minimal.msi", msiOpenDatabaseModeReadOnly) : CheckError

Dim database2 : Set database2 = installer.OpenDatabase(TransformName & ".msi", msiOpenDatabaseModeReadOnly) : CheckError

Dim transform : transform = TransformName & ".mst"

database2.GenerateTransform database1, transform : CheckError

database2.CreateTransformSummaryInfo database1, transform, msiTransformErrorNone, msiTransformValidationNone : CheckError

Set database1 = Nothing

Set database2 = Nothing

' Embed Transform into the database

Set database = installer.OpenDatabase("Minimal.msi", msiOpenDatabaseModeTransact) : CheckError

Set view = database.OpenView("SELECT `Name`,`Data` FROM _Storages") : CheckError

view.Execute : CheckError

Set record = installer.CreateRecord(2)

record.StringData(1) = TransformName

record.SetStream 2, TransformName & ".mst" : CheckError

view.Modify msiViewModifyAssign, record : CheckError

database.Commit : CheckError

Set database = Nothing

The command line to install the product applying the Transform will look like this:

msiexec /i Minimal.msi TRANSFORMS=:Dev

or

msiexec /i Minimal.msi TRANSFORMS=:Test