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.
Tip: 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
}
I’m glad to see I wasn’t crazy in the way that I did this! I just wish I found this post sooner 🙂
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!
Sure! Throw it my way. It may take a couple of weeks for me to get to it. I’m rebuilding my homelab….AGAIN! 🙂
Hi Im trying to insert into registry table with this script it does not work any ideas?
Hi Rob. I’d need more information than that! What error are you getting?
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
…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?
sorry code was
$ValuesList += “INSERT INTO `Registry` (`Registry`, `Root`, `Key`, `Name`, `Value`, `Component_`) VALUES (‘SampleReg’, 2, ‘Software\AlkaneTest’, ‘testName’, ‘testValue’, ‘AuditRegKeys’)”
its not showing the escape characters
Have you get any solution on the same?
Yeah I’ve just realised it’s not showing the backticks in these comments. Drop the script to kae.travis@alkanesolutions.co.uk and I’ll have a quick look. Otherwise you may get a quicker response asking over at ITNinja…
Issue resolved via usage of double backticks. See tip in blog post.
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
Hi Jim. Thanks for the comment. Drop me your script in an email and I’ll try and take a look if I get a second spare. Thanks.
Jim, this should help: http://www.alkanesolutions.co.uk/2017/02/10/write-windows-installer-binary-stream-using-powershell/
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
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.
How to add new component for windows installer using powershell script?
How to add new component for windows installer using powershell script?
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()
“`
Great work, and thanks for sharing!