Geeks With Blogs
Alex Hildyard

In the last article we looked at a simple way to configure MSI-based applications for different environments by swapping in an appropriate configuration file at install time. I explained however that we could improve on this with a little more work. What we'll now do is change our solution so that the individual configuration items are read directly from the MSI file. We'll do this with our own Custom Action, utilising a custom table of key/value/environment tuples. So we need to make the following changes:

 

Moving Configuration Data into the MSI

  • Add a new WiX file to our existing MyApplicationInstaller project. Call it ConfigSettings.wxs, and define a Custom table with Id, Key, Data and Environment columns. Populate this with settings for the environments we have already defined

-- ConfigSettings.wxs --

 

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

<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">

<Fragment>

<CustomTable Id="ConfigSettings">

<Column Id="Id" Width="4" PrimaryKey="yes" Type="int" />

<Column Id="Token" Type="string" />

<Column Id="Data" Type="string" />

<Column Id="Environment" Type="string" />

<Row>

<Data Column="Id">1</Data>

<Data Column="Token">MYCONFIGURATION</Data>

<Data Column="Data">UAT</Data>

<Data Column="Environment">UAT</Data>

</Row>

<Row>

<Data Column="Id">2</Data>

<Data Column="Token">MYCONFIGURATION</Data>

<Data Column="Data">Production</Data>

<Data Column="Environment">PRD</Data>

</Row>

</CustomTable>

</Fragment>

</Wix>

 

  • Add a new C# Custom Action project called InstallerCustomActions . For the moment all we need is a single Custom action, which extracts the various configuration settings for a given environment and caches them in session properties:

-- CustomAction.cs --

 

[CustomAction]

public static ActionResult AssignEnvironmentSettings(Session session)

{

session.Log("Begin AssignEnvironmentSettings");

session.Log(String.Format("Assigning configuration settings for {0}", session["APPENV"]));

// Extract the configuration settings from the ConfigSettings table

using (View v = session.Database.OpenView(String.Format(

"SELECT Token, Data FROM ConfigSettings WHERE Environment = '{0}'",

session["APPENV"]

)))

{

v.Execute();

using (Record r = v.Fetch())

{

// Assign the key/value pair to a session-level MSI property

string token = r.GetString(1);

string data = r.GetString(2);

session.Log(String.Format("Assigning: {0} = {1}", token, data));

session[token] = data;

r.Close();

}

v.Close();

}

return ActionResult.Success;

}

  • Add a WiX Binary reference to the new InstallerCustomActions DLL in Product.wxs, add a Custom action to invoke the AssignEnvironmentSettings entry point within the Fragment defined in ConfigSettings.wxs, then in Product.wxs once again schedule the Custom action to run early within the installation. We put the Custom action declaration in ConfigSettings.wxs rather than Product.wxs to force the WiX compiler to embed our custom table in the MSI; without this reference everything would compile just fine, but the table wouldn't be included -- probably resulting in a 1603 error at install time

-- Product.wxs --

 

<InstallExecuteSequence>

<Custom Action="SetEnvironmentData" After="CostInitialize"/>

</InstallExecuteSequence>

<Binary Id="InstallerCAs" SourceFile="..\InstallerCustomActions\bin\InstallerCustomActions.CA.dll"/>

-- ConfigSettings.wxs –

</CustomTable>

<CustomAction Id="SetEnvironmentData" BinaryKey="InstallerCAs" DllEntry="AssignEnvironmentSettings"/>

</Fragment>

  • Edit MarkUpMyApplication.wxs, removing the conditional file deployments and replacing them with new GUIDs and an XmlFile markup directive; this means we also need to include the UtilExtension schema with an appropriate namespace declaration, and we need to add a Project reference to WiXUtilExtension.dll (which is in the /bin directory beneath the WiX 3.7 installation folder)

-- MarkUpMyApplication.wxs --

 

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

<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">

<Fragment>

<ComponentGroup Id="MarkUpMyApplication">

<!-- Mark up the myConfiguration attribute for the deployment environment in question -->

<Component Id="cmp1E212C2FA32A432F8C8349C02B45EDAB" Directory="MYAPPLICATIONINSTALLFOLDER" Guid="{78D12327-30D1-4E4C-9F0D-51B81A68C895}">

<util:XmlFile Id="xmlA1F61C90827D458E80D48E3A6948B360" Action="setValue" ElementPath="//appSettings/add[\[]@key='myConfiguration'[\]]" Value="[MYCONFIGURATION]" File="[MYAPPLICATIONINSTALLFOLDER]MyApplication.exe.config" Name="value" SelectionLanguage="XPath" Sequence="2"/>

</Component>

</ComponentGroup>

</Fragment>

</Wix>

  • Delete App.PRD.config and App.UAT.config from the MyApplication project, since we won’t be needing these files any more

 

  • Add a <CreateFolder/> node to the single Component within this ComponentGroup; without this you'll get an ICE18 error since our KeyPath is a directory that we haven't explicitly created

<Component Id="cmp1E212C2FA32A432F8C8349C02B45EDAB" Directory="MYAPPLICATIONINSTALLFOLDER" Guid="{78D12327-30D1-4E4C-9F0D-51B81A68C895}">

<CreateFolder/>

<util:XmlFile Id="xmlA1F61C90827D458E80D48E3A6948B360" Action="setValue" ElementPath="//appSettings/add[\[]@key='myConfiguration'[\]]" Value="[MYCONFIGURATION]" File="[MYAPPLICATIONINSTALLFOLDER]MyApplication.exe.config" Name="value" SelectionLanguage="XPath" Sequence="2"/>

</Component>

  • Finally, reinstate the code responsible for harvesting MyApplication.exe.config in MyApplicationInstaller.wixproj. You’ll recall that we removed this when we migrated our MSI installer from single-instance to multi-instance, so there would be no ambiguity as to which file would get deployed. You'll notice actually that after our most recent refactoring the two ICE warnings have now vanished, because there's no longer any doubt as to whether MyApplication.exe.config will get overwritten or not. We've removed the conditions and simply said that we'll mark up the myConfiguration attribute with whatever settings were specified for the relevant installation environment. That means we need to make sure the file gets harvested explicitly by heat.exe:

<ItemGroup>

<MyApplicationFiles Include="$(MSBuildProjectDirectory)\..\MyApplication\bin\$(Configuration)\*.exe" />

<MyApplicationFiles Include="$(MSBuildProjectDirectory)\..\MyApplication\bin\$(Configuration)\*.exe.config" />

<MyApplicationFiles Remove="$(MSBuildProjectDirectory)\..\MyApplication\bin\$(Configuration)\*vshost.*" />

</ItemGroup>

You can test the new MSI with the same command lines we used before:

 

msiexec /i MyApplicationInstaller.msi ADDLOCAL="ApplicationCopyFiles" TARGETDIR="c:\temp\UAT" MSINEWINSTANCE=1 TRANSFORMS=:UAT /l*V log.txt

msiexec /i MyApplicationInstaller.msi ADDLOCAL="ApplicationCopyFiles" TARGETDIR="c:\temp\PRD" MSINEWINSTANCE=1 TRANSFORMS=:PRD /l*V log.txt

 

Handling uninstallation

Those of you who have borne with me thus far may be wondering what happens when we uninstall a particular environment instance. It’s obviously easiest just to open up the ARP and remove the instance from there, but typically you will want to uninstall your application MSI programmatically, so that your deployment scripts can put in a fresh instance. Applications whose instances install without error will often run into trouble on uninstallation because configuration data specified at installation time may no longer be available. This is particularly true for applications relying on a UI for dynamic feature selection, and this is the reason Rob Mensching specifically addresses the problem with his “Remember property” pattern.

In our case, command lines like the following will actually work without issue:

 

msiexec /x MyApplicationInstaller.msi ADDLOCAL="ApplicationCopyFiles" TRANSFORMS=:UAT /l*V log.txt

msiexec /x MyApplicationInstaller.msi ADDLOCAL="ApplicationCopyFiles" TRANSFORMS=:PRD /l*V log.txt

The reason they work is because, so long as we pass a valid transform on the command line, this transform will bubble through into the APPENV property due to the internal operation of InstanceTransforms. Meanwhile SetEnvironmentData will always get called, regardless of whether it’s an install, repair or uninstall, and since all this action needs to work its magic is a valid APPENV, all our session properties will be set correctly. You can download the code for part 3 here.

Posted on Tuesday, April 9, 2013 4:20 PM | Back to top


Comments on this post: Creating Multi-Environment Windows Installers with Visual Studio 2012 and Wix 3.7: Part 3

No comments posted yet.
Your comment:
 (will show your gravatar)


Copyright © Alex Hildyard | Powered by: GeeksWithBlogs.net