Home » General Scripting and Coding » PowerShell » Generate a Windows Installer transform file (MST) using Powershell

Generate a Windows Installer transform file (MST) using Powershell

Posted on by

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
}

 

31 thoughts on “Generate a Windows Installer transform file (MST) using Powershell

  1. 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

  2. …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.

    • Yes I have tried this.

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

      its still not having it same error as before. Any other suggestions?

          • I add this but it is showing error Exception calling “InvokeMember” with “5” argument(s): “OpenView,Sql”
            Could you please check once. Thanks In advance

            #Insert a Registry
            $queryReg = “INSERT INTO “Registry“ (“Registry“,“Root“,“Key“,“Name“,“Value“,“Component_“) VALUES (‘Registry1′,’2′,’Software\Chiranjit’,’Installed’,’1′,’AAA_AddReg’)”

            $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($ViewCom) | Out-Null

          • Please read the tip again. You are surrounding table names and columns with a quote (“) but you should be surrounding them with a double back-tick (“) instead. For example: "INSERT INTO ``Registry`` (``Registry``,

          • Upon looking again (i don’t think the back-ticks paste properly in this comments section) you need to:
            1. Remove the quotes around “2” (the ‘root’ column.). This column is an Integer data type and doesn’t need quotes around it. Also be careful of the quotes around “1” (the ‘value’ column) because they look different. This is a working example: $queryReg = "INSERT INTO ``Registry`` (``Registry``,``Root``,``Key``,``Name``,``Value``,``Component_``) VALUES ('Registry1',2,'Software\Chiranjit','Installed','1','AAA_AddReg')"

          • I put that only but it showing error
            #Insert a Registry
            $queryReg = “INSERT INTO “Registry“ (“Registry“,“Root“,“Key“,“Name“,“Value“,“Component_“) VALUES (‘ ‘,’2′,’Software\Chiranjit’,’Installed’,’1′,’AAA_AddReg’)”

            $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

          • Hi Kae,

            Can you please look my script actually its now working. Can i send one mail to you .

  3. 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

  4. 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

  5. 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.

     

  6. HI can you send me the script for adding registry . I added component and feature also. But when adding registry , sql error is coming.. Please paste the working command .Thanks

Comments have now been disabled. If you have a question to ask about this post please ask the community!