Kae Travis

Generate a Windows Installer transform file (MST) using PowerShell

I recently needed to generate a Windows Installer transform file (MST) using PowerShell.  I’ve ripped out some excerpts below to demonstrate inserting rows, querying tables, retrieving properties and generating a transform.  Using this example for reference, you should be able to accomplish most tasks using Powershell and the Windows Installer object.

tipTip: Make sure you enclose all table and column names with a double backtick to avoid ‘OpenView,Sql’ errors during OpenView() calls!  By default a single backtick in Powershell acts as an escape character, so we are required to use two of these.

$msiOpenDatabaseModeReadOnly = 0
$msiOpenDatabaseModeTransact = 1
$msiTransformErrorNone = 0
$msiTransformValidationNone = 0
$msiFolder = "C:\alkaneMSI\"
$database1Path = $msiFolder + "example.msi"
$database2Path = $database1Path + "_bak"
$MSTPath = $msiFolder + "New_MST.mst"
 
#If backup doesn't already exist, make a copy to make our changes to
if (!(Test-Path -Path $database2Path))
{
	Copy-Item -Path $database1Path -Destination $database2Path
}
	
#Remove MST if already exists
If (Test-Path $MSTPath){
		Remove-Item $MSTPath
}

#open original MSI in ReadOnly mode
$windowsInstaller = New-Object -ComObject WindowsInstaller.Installer         	 
$database1 = $windowsInstaller.GetType().InvokeMember(
	"OpenDatabase", 
	"InvokeMethod", 
	$Null, 
	$windowsInstaller, 
	@($database1Path, $msiOpenDatabaseModeReadOnly)
)  

#open 'backup' MSI in transact mode
$database2 = $windowsInstaller.GetType().InvokeMember(
	"OpenDatabase", 
	"InvokeMethod", 
	$Null, 
	$windowsInstaller, 
	@($database2Path, $msiOpenDatabaseModeTransact)
) 
	
#use the TablePersist method to see if the CustomAction table exists
$tableExists =  $database2.GetType().InvokeMember(
	"TablePersistent",
	"GetProperty",
	$Null,
	$database2,
	"CustomAction"
)	

#If CustomAction table does not exist
if ([int]$tableExists -ne 1)
{
	#Create CustomAction table
	$query = "CREATE TABLE ``CustomAction`` ( ``Action`` CHAR(72) NOT NULL, ``Type`` SHORT NOT NULL, ``Source`` CHAR(64), ``Target`` LONGCHAR PRIMARY KEY ``Action``)"
	$View = $database2.GetType().InvokeMember(
		"OpenView",
		"InvokeMethod",
		$Null,
		$database2,
		($query)
	)
	$View.GetType().InvokeMember("Execute", "InvokeMethod", $Null, $View, $Null)		
	$View.GetType().InvokeMember("Close", "InvokeMethod", $Null, $View, $Null)
	[System.Runtime.Interopservices.Marshal]::ReleaseComObject($View) | Out-Null
}

#Insert a test Custom Action
$query = "INSERT INTO ``CustomAction`` (``Action``,``Type``,``Source``,``Target``) VALUES ('TestCA',38,'','MsgBox ""test""')"

$View = $database2.GetType().InvokeMember(
	"OpenView",
	"InvokeMethod",
	$Null,
	$database2,
	($query)
)
$View.GetType().InvokeMember("Execute", "InvokeMethod", $Null, $View, $Null)		
$View.GetType().InvokeMember("Close", "InvokeMethod", $Null, $View, $Null)
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($View) | Out-Null

#We want to sequence the Custom Action to run after CostFinalize, so we get the sequence number for CostFinalize	
$query = "SELECT ``Sequence`` FROM ``InstallExecuteSequence`` WHERE ``Action`` = 'CostFinalize'"
$View = $database2.GetType().InvokeMember(
	"OpenView",
	"InvokeMethod",
	$Null,
	$database2,
	($query)
)
$View.GetType().InvokeMember("Execute", "InvokeMethod", $Null, $View, $Null)

$record = $View.GetType().InvokeMember(
	"Fetch",
	"InvokeMethod",
	$Null,
	$View,
	$Null
)

$IESequence = [int]0
while ($record -ne $null) {
	$IESequence = [int]$record.GetType().InvokeMember("StringData", "GetProperty", $Null, $record, 1)
	
	$record = $View.GetType().InvokeMember(
		"Fetch",
		"InvokeMethod",
		$Null,
		$View,
		$Null
	)
}
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($View) | Out-Null

#We insert the Custom Action into the InstallExecuteSequence table, adding one to the sequence number retrieved from CostFinalize
$query = "INSERT INTO ``InstallExecuteSequence`` (``Action``,``Sequence``) VALUES ('TestCA','" + ($IESequence+1) + "')"
$View = $database2.GetType().InvokeMember(
	"OpenView",
	"InvokeMethod",
	$Null,
	$database2,
	($query)
)
$View.GetType().InvokeMember("Execute", "InvokeMethod", $Null, $View, $Null)		
$View.GetType().InvokeMember("Close", "InvokeMethod", $Null, $View, $Null)
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($View) | Out-Null	

#Commit the changes to our backup database
$database2.GetType().InvokeMember("Commit", "InvokeMethod", $Null, $database2, $Null)
	
#Generate a transform (the difference between our original MSI and our Backup MSI)	
$transformSuccess = $database2.GetType().InvokeMember(
	"GenerateTransform", 
	"InvokeMethod", 
	$Null, 
	$database2, 
	@($database1,$MSTPath)
)  
			
#Create a Summary Information Stream for the MST
$transformSummarySuccess = $database2.GetType().InvokeMember(
	"CreateTransformSummaryInfo", 
	"InvokeMethod", 
	$Null, 
	$database2, 
	@($database1,$MSTPath, $msiTransformErrorNone, $msiTransformValidationNone)
)  

#Release objects from memory
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($database1) | Out-Null 
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($database2) | Out-Null
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($windowsInstaller) | Out-Null

#Delete backup database
If (Test-Path $database2Path){
	Remove-Item $database2Path
}

 

Generate a Windows Installer transform file (MST) using PowerShell
Generate a Windows Installer transform file (MST) using PowerShell

23 thoughts on “Generate a Windows Installer transform file (MST) using PowerShell

  1. Yeah, partly my fault.  I must dedicate more time to SEO on this blog and you would have found it sooner! 🙂  Hey, I’ve just released an App-V Conversion tool here: http://www.alkanesolutions.co.uk/2014/12/29/virtualisr-app-v-4-6-app-v-5-automated-sequencing/.  If you’re interesting on writing a blog entry on it let me know and I’ll throw you some free licenses!

  2. Hi

    The script works well when inserting into other tables. EG “INSERT INTO FeatureComponents (Feature_, Component_) VALUES (‘Program’,’AuditKeys’)”

    But when issuing this as the query.

    “INSERT INTO Registry (Registry, Root, Key, Name, Value, Component_) VALUES (‘SampleReg’, 2, ‘Software\AlkaneTest’, ‘testName’, ‘testValue’, ‘AuditRegKeys’)”

    I get this.

    Exception calling “InvokeMember” with “5” argument(s): “OpenView,Sql”
    At line:61 char:1
    + $View = $database2.GetType().InvokeMember(
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : COMException

    COM object that has been separated from its underlying RCW cannot be used.
    At line:69 char:1
    + $View.GetType().InvokeMember(“Execute”, “InvokeMethod”, $Null, $View, …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : OperationStopped: (:) [], InvalidComObjectException
    + FullyQualifiedErrorId : System.Runtime.InteropServices.InvalidComObjectException

    COM object that has been separated from its underlying RCW cannot be used.
    At line:70 char:1
    + $View.GetType().InvokeMember(“Close”, “InvokeMethod”, $Null, $View, $ …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : OperationStopped: (:) [], InvalidComObjectException
    + FullyQualifiedErrorId : System.Runtime.InteropServices.InvalidComObjectExceptio

  3. …try enclosing your table names and column names with back-ticks like in the examples…..CREATE TABLE `CustomAction` ( `Action` etc.

    I think you’ll find that some columns require this (like the Registry.Key column).  That’s probably where your error is.  It’s probably a best practise to enclose table names and column names like this at all times.

  4. Hi,

    Your post has help me so much updating my automation scripts from vbs to Powershell!

    I have one issue when trying to add a custom action into my mst as the vbscript needs to be added to the binary table and everything I have tried using CreateRecord and SetStream has failed. I have searched everywhere on google and can’t find an answer for powershell.

    Hoping you can point me in the wright direction.

    Thanks,

    Jim

  5. Hi,
    Many thanks for your script, I have tried and it worked wonders.
    But I have a small issue now.
    I am trying to create registry entries in the msi database. I am getting the sql error. Below is my code snippet.
    $queryReg = “INSERT INTO “Registry“ (“Registry“,“Root“,“Key“,“Name“,“Value“,“Component_“) VALUES (‘Branding’,’2′,’Software\test’,’testname’,’1234′,’Branding”
    $ViewReg = $database2.GetType().InvokeMember(“OpenView”,”InvokeMethod”,$Null,$database2,($queryReg))
    $ViewReg.GetType().InvokeMember(“Execute”, “InvokeMethod”, $Null, $ViewReg, $Null) 
    $ViewReg.GetType().InvokeMember(“Close”, “InvokeMethod”, $Null, $ViewReg, $Null)
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($ViewReg) | Out-Null
    Error Message:
    Exception calling “InvokeMember” with “5” argument(s): “OpenView,Sql”
    At C:\Scripts\New folder\MSI\Untitled5.ps1:71 char:1
    + $ViewReg = $database2.GetType().InvokeMember(“OpenView”,”InvokeMethod”,$Null,$da …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : COMException

    You cannot call a method on a null-valued expression.
    At C:\Scripts\New folder\MSI\Untitled5.ps1:72 char:1
    + $ViewReg.GetType().InvokeMember(“Execute”, “InvokeMethod”, $Null, $ViewReg, $Nul …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

  6. Hi.

    Firstly check the tip in this post for double back ticks.

    Secondly check your SQL statement against the golden rules mentioned here: http://www.alkanesolutions.co.uk/2012/11/30/tutorial-1-introduction-to-msi-scripting-and-golden-rules/

    Your copy and paste doesn’t look to have rendered correctly since your quotes are incorrect and you’re missing a closing bracket.

    (‘Branding’,’2′,’Software\test’,’testname’,’1234′,’Branding”

    should be

    (‘Branding’,’2’,’Software\test’,’testname’,’1234’,’Branding’)”

    Be VERY careful of the quotes and back ticks.  They don’t paste very well in these comments.

     

  7. Thanks, this is great! I ran into an issue with this, though: I’m generating a MST to remove some rows in the `CustomAction` and `InstallExecuteSequence` tables to prevent the software from starting automatically upon installation. I ran SQL statements to delete those rows, but it also deleted any row referenced by the the deleted rows.

    Orca had no issue deleting only the desired rows, so I used the excellent API Monitor v2 by Rohitab to inspect what it was actually doing. It turns out that rather than copying the entire MSI to create a comparison database, it created a new database into which it merged the original database.

    Duplicating this process in Powershell resulted in a MST with only the desired changes.

    I’m using the WIX toolset library to avoid calling COM functions directly, but here’s what I came up with (not sure if the code will format properly…

    “`
    $originalDB = [WixToolset.Dtf.WindowsInstaller.Database]::new($msiReferenceFilePath, [WixToolset.Dtf.WindowsInstaller.DatabaseOpenMode]::ReadOnly)
    $updatedDB = [WixToolset.Dtf.WindowsInstaller.Database]::new($msiComparisonFilePath, [WixToolset.Dtf.WindowsInstaller.DatabaseOpenMode]::Create)

    $updatedDB.Merge($originalDB)

    $mstQueries = @(
    ‘Delete From `InstallExecuteSequence` where `Action` = ”runAfterFinish”’
    ‘Delete From `InstallExecuteSequence` where `Action` = ”runAfterFinishPerMachine”’
    ‘Delete From `InstallExecuteSequence` where `Action` = ”scheduleUninstallPerUser”’
    ‘Delete From `CustomAction` where `Action` = ”runAfterFinishPerMachine”’
    ‘Delete From `CustomAction` where `Action` = ”runAfterFinish”’
    ‘Delete From `CustomAction` where `Action` = ”scheduleUninstallPerUser”’
    ‘Update `Property` set `Value` = ”1” where `Property` = ”ALLUSERS”’
    ‘Update `Property` set `Value` = ”0” where `Property` = ”ACCEPTEULA”’
    ‘Update `Property` set `Value` = ”0” where `Property` = ”MSIINSTALLPERUSER”’
    ‘Delete From `Property` where `Property` = ”SecureCustomProperties”’
    )

    foreach ($queryText in $mstQueries) {
    $updatedDB.Execute($queryText)
    }

    $transformSuccess = $updatedDB.GenerateTransform($originalDB, $mstPath)

    $updatedDB.CreateTransformSummaryInfo($originalDB, $mstPath, [WixToolset.Dtf.WindowsInstaller.TransformErrors]::None, [WixToolset.Dtf.WindowsInstaller.TransformValidations]::None)

    $originalDB.Close()
    $updatedDB.Close()
    “`

Leave a Reply