There is an elegant solution that took quite a bit of effort to work out being someone not very familiar with Windows Installer or InstallShield. The Events tab of the Release object on InstallShield's Releases page has a Precompression event, during which time it's possible to modify the MSI file before it gets compressed into Setup.exe. I have entered this command into the Precompression event:
"<ISProjectFolder>\UpdateMSI.exe" "<ISReleasePath>\<ISProductConfigName>\<ISReleaseName>\DiskImages\Disk1\Dummy.msi"
Where Dummy.msi
is the msi generated by InstallShield before it gets compressed (or if the release is configured not to compress the result).
I created a VB.NET program called UpdateMSI and put the output in C:\InstallShield 2015 Projects\
. What this program does is essentially extract all the instance transforms generated and embedded into the MSI file by InstallShield, and updates them to also update the GUIDs for the component(s). Right now I have only one component which updates the registry so I am updating just the one component to have a separate GUID for each instance (up to 9 instances plus the default instance).
The result is a setup program that cleanly installs and un-installs these components by retaining separate GUIDs for them without having to duplicate them in the InstallShield project.
The code for UpdateMSI looks like this.
Imports System.Runtime.InteropServices
Module Main
Private Declare Auto Function MsiOpenDatabase Lib "msi.dll" (ByVal szDatabasePath As String, ByVal szPersist As IntPtr, <Out> ByRef phDatabase As IntPtr) As UInt32
Private Declare Auto Function MsiCloseHandle Lib "msi.dll" (ByVal hAny As Integer) As UInt32
Private Declare Auto Function MsiDatabaseOpenView Lib "msi.dll" (ByVal hDatabase As IntPtr, ByVal szQuery As String, <Out> ByRef phView As IntPtr) As UInt32
Private Declare Auto Function MsiViewExecute Lib "msi.dll" (ByVal hView As IntPtr, ByVal hRecord As IntPtr) As UInt32
Private Declare Auto Function MsiViewFetch Lib "msi.dll" (ByVal hView As IntPtr, <Out> ByRef phRecord As IntPtr) As UInt32
Private Declare Auto Function MsiRecordGetString Lib "msi.dll" (ByVal hRecord As IntPtr, iField As UInt32, ByVal szValueBuf As System.Text.StringBuilder, ByRef pcchValueBuf As UInt32) As UInt32
Private Declare Auto Function MsiViewModify Lib "msi.dll" (ByVal hView As IntPtr, eModifyMode As IntPtr, hRecord As IntPtr) As UInt32
Private Declare Auto Function MsiRecordSetString Lib "msi.dll" (ByVal hRecord As IntPtr, iField As UInt32, szValue As String) As UInt32
Private Declare Auto Function MsiDatabaseCommit Lib "msi.dll" (ByVal hDatabase As IntPtr) As UInt32
Private Declare Auto Function MsiViewClose Lib "msi.dll" (ByVal hView As IntPtr) As UInt32
Private Declare Auto Function MsiDatabaseApplyTransform Lib "msi.dll" (ByVal hDatabase As IntPtr, ByVal szTransformFile As String, ByVal iErrorConditions As Integer) As UInt32
Private Declare Auto Function MsiDatabaseGenerateTransform Lib "msi.dll" (ByVal hDatabase As IntPtr, ByVal hDatabaseReference As IntPtr, ByVal szTransformFile As String, ByVal iReserved As Integer, ByVal iReserved As Integer) As UInt32
Private Declare Auto Function MsiCreateTransformSummaryInfo Lib "msi.dll" (ByVal hDatabase As IntPtr, ByVal hDatabaseReference As IntPtr, ByVal szTransformFile As String, ByVal iErrorConditions As Integer, ByVal iValidation As Integer) As UInt32
Private Declare Auto Function MsiRecordSetStream Lib "msi.dll" (ByVal hRecord As IntPtr, ByVal iField As UInt32, ByVal szFilePath As String) As UInt32
Private Const MSIDBOPEN_READONLY As Integer = 0
Private Const MSIDBOPEN_TRANSACT As Integer = 1
Private Const MSIDBOPEN_DIRECT As Integer = 2
Private Const ERROR_SUCCESS As Integer = 0
Private Const ERROR_NO_MORE_ITEMS As Integer = 259
Private Const MSIMODIFY_UPDATE As Integer = 2
Private Enum MSITRANSFORM_VALIDATE
None = 0
Language = 1
Product = 2
MajorVersion = 8
MinorVersion = &H10
UpdateVersion = &H20
NewLessBaseVersion = &H40
NewLessEqualBaseVersion = &H80
NewEqualBaseVersion = &H100
NewGreaterEqualBaseVersion = &H200
NewGreaterBaseVersion = &H400
UpgradeCode = &H800
End Enum
Const RegComponentGuid As String = "{3AACE297-A264-4223-A0AF-C5A20D37551F}"
Dim RegComponentInstGuids As String() = { _
"{12456A37-51F3-4F53-A19E-34DF8CB1B063}", _
"{0F00D476-E1F2-4B5D-85C4-D380A33BB78E}", _
"{EFC3C9D6-9C2B-43E9-84A3-837C86BE5DD8}", _
"{1398A80D-681D-4726-9972-8DEC8B030B4A}", _
"{FF610463-5E35-4059-A3C4-EB51A7CABA1D}", _
"{C4A43C73-5791-4B17-906C-93240AAB2F03}", _
"{12E37949-704F-4A92-A7D2-5B7B94554505}", _
"{E9175D7C-BC4E-4AC1-A79D-6B314EAB5D45}", _
"{0A1E0D5C-44CC-4199-9C4E-FE23A1136B60}"}
Function Main(args As String()) As Integer
If args.Count <> 1 Then
Console.Error.WriteLine(String.Format("{0} <MSI File>", System.Reflection.Assembly.GetExecutingAssembly().GetName().Name))
Console.ReadLine()
Return 1
End If
Main = UpdateTransforms(args(0))
End Function
Public Sub CheckResult(result As Integer, Optional ByVal failureName As String = "MSI call")
If result <> ERROR_SUCCESS Then
Throw New ApplicationException(String.Format("{0} failed with code {1}", failureName, result))
End If
End Sub
Private Function UpdateTransforms(msiFile As String)
Dim hDB As IntPtr = IntPtr.Zero
Dim hDBOriginal As IntPtr = IntPtr.Zero
Dim hView As IntPtr = IntPtr.Zero
Dim mainResult As Integer = 0
Dim hRec As IntPtr = IntPtr.Zero
Dim instanceNames As IEnumerable(Of String) = Nothing
Try
instanceNames = GetEmbeddedTransforms(msiFile)
Console.WriteLine("Found {0} instances embedded in {1}", instanceNames.Count, msiFile)
If instanceNames.Count > RegComponentInstGuids.Length Then
Console.Error.WriteLine("More instances were found than pre-defined GUIDs")
Console.ReadLine()
Return 4
End If
' For each embedded instance, modify the MST for that instance to also modify component GUIDs
Dim componentIndex As Integer = 0
For Each instanceName In instanceNames
Dim msiCopy As String = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "InstanceIdTmp.msi")
System.IO.File.Copy(msiFile, msiCopy)
CheckResult(MsiOpenDatabase(msiCopy, MSIDBOPEN_TRANSACT, hDB), "MsiOpenDatabase")
CheckResult(MsiDatabaseApplyTransform(hDB, String.Format(":{0}", instanceName), 0), "MsiDatabaseApplyTransform")
CheckResult(MsiDatabaseOpenView(hDB, String.Format("UPDATE `Component` SET `Component`.`ComponentId`='{0}' WHERE `Component`.`ComponentId`='{1}'", RegComponentInstGuids(componentIndex), RegComponentGuid), hView), "MsiDatabaseOpenView")
CheckResult(MsiViewExecute(hView, IntPtr.Zero), "MsiViewExecute")
CheckResult(MsiViewClose(hView), "MsiViewClose")
CheckResult(MsiDatabaseCommit(hDB), "MsiDatabaseCommit")
CheckResult(MsiOpenDatabase(msiFile, MSIDBOPEN_TRANSACT, hDBOriginal), "MsiOpenDatabase")
Dim mstFile As String = System.IO.Path.Combine(System.IO.Path.GetTempPath(), instanceName)
CheckResult(MsiDatabaseGenerateTransform(hDB, hDBOriginal, mstFile, 0, 0), "MsiDatabaseGenerateTransform")
CheckResult(MsiCreateTransformSummaryInfo(hDB, hDBOriginal, mstFile, 0, MSITRANSFORM_VALIDATE.UpgradeCode), "MsiCreateTransformSummaryInfo")
CheckResult(MsiCloseHandle(hView), "MsiCloseHandle")
hView = IntPtr.Zero
CheckResult(MsiDatabaseOpenView(hDBOriginal, String.Format("SELECT Data FROM `_Storages` WHERE Name='{0}'", instanceName), hView), "MsiDatabaseOpenView")
Console.WriteLine("Updating component {0} in instance {1} to use {2}", RegComponentGuid, componentIndex, RegComponentInstGuids(componentIndex))
CheckResult(MsiViewExecute(hView, IntPtr.Zero), "MsiViewExecute")
CheckResult(MsiViewFetch(hView, hRec), "MsiViewFetch")
CheckResult(MsiRecordSetStream(hRec, 1, mstFile), "MsiRecordSetStream")
CheckResult(MsiViewModify(hView, 2, hRec), "MsiViewModify")
CheckResult(MsiCloseHandle(hRec), "MsiCloseHandle")
hRec = IntPtr.Zero
CheckResult(MsiViewClose(hView), "MsiViewClose")
CheckResult(MsiDatabaseCommit(hDBOriginal), "MsiDatabaseCommit")
If System.IO.File.Exists(mstFile) Then System.IO.File.Delete(mstFile)
CheckResult(MsiCloseHandle(hView), "MsiCloseHandle")
hView = IntPtr.Zero
CheckResult(MsiCloseHandle(hDBOriginal), "MsiCloseHandle")
hDBOriginal = IntPtr.Zero
CheckResult(MsiCloseHandle(hDB), "MsiCloseHandle")
hDB = IntPtr.Zero
System.IO.File.Delete(msiCopy)
componentIndex += 1
Next
Catch ex As System.Exception
Console.Error.WriteLine(ex.ToString())
Console.ReadLine()
mainResult = 2
Finally
Try
If hRec <> IntPtr.Zero Then CheckResult(MsiCloseHandle(hRec), "MsiCloseHandle")
If hDBOriginal <> IntPtr.Zero Then CheckResult(MsiCloseHandle(hDBOriginal), "MsiCloseHandle")
If hView <> IntPtr.Zero Then CheckResult(MsiCloseHandle(hView), "MsiCloseHandle")
Catch ex As Exception
Console.Error.WriteLine(ex.ToString())
Console.ReadLine()
mainResult = 3
Finally
If hDB <> IntPtr.Zero Then
CheckResult(MsiCloseHandle(hDB), "MsiCloseHandle")
End If
Dim msiCopy As String = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "InstanceIdTmp.msi")
If System.IO.File.Exists(msiCopy) Then System.IO.File.Delete(msiCopy)
If instanceNames IsNot Nothing Then
For Each instanceName In instanceNames
If System.IO.File.Exists(instanceName) Then System.IO.File.Delete(instanceName)
Next
End If
End Try
End Try
Return mainResult
End Function
Private Function GetEmbeddedTransforms(msiFile As String) As IEnumerable(Of String)
Dim hDB As IntPtr = IntPtr.Zero
Dim hView As IntPtr = IntPtr.Zero
Dim hRec As IntPtr = IntPtr.Zero
Dim instanceNames As New LinkedList(Of String)
Try
CheckResult(MsiOpenDatabase(msiFile, MSIDBOPEN_READONLY, hDB), "MsiOpenDatabase")
CheckResult(MsiDatabaseOpenView(hDB, "SELECT Name FROM `_Storages`", hView), "MsiDatabaseOpenView")
CheckResult(MsiViewExecute(hView, IntPtr.Zero), "MsiViewExecute")
Do
Dim fetchResult = MsiViewFetch(hView, hRec)
If fetchResult = ERROR_NO_MORE_ITEMS Then Exit Do
CheckResult(fetchResult)
Dim instMstName As New System.Text.StringBuilder(256)
CheckResult(MsiRecordGetString(hRec, 1, instMstName, instMstName.Capacity - 1), "MsiRecordGetString")
If instMstName.ToString() Like "InstanceId#*.mst" Then
instanceNames.AddLast(instMstName.ToString())
End If
CheckResult(MsiCloseHandle(hRec), "MsiCloseHandle")
hRec = IntPtr.Zero
Loop
CheckResult(MsiViewClose(hView), "MsiViewClose")
Catch ex As System.Exception
Console.Error.WriteLine(ex.ToString())
Console.ReadLine()
Finally
If hRec <> IntPtr.Zero Then CheckResult(MsiCloseHandle(hRec), "MsiCloseHandle")
If hView <> IntPtr.Zero Then CheckResult(MsiCloseHandle(hView), "MsiCloseHandle")
If hDB <> IntPtr.Zero Then CheckResult(MsiCloseHandle(hDB), "MsiCloseHandle")
End Try
Return instanceNames
End Function
End Module