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.
Comments
Anonymous
June 30, 2009
The comment has been removedAnonymous
June 30, 2009
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.Anonymous
July 01, 2009
Thank you for fast answer. Custom Tables became clearer for me.Anonymous
April 28, 2010
The comment has been removedAnonymous
April 30, 2010
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.