So near, and yet so far...
I've been trying to find a way to generate an MST file on-the-fly as part of deployment automation. The script below works, in that it generates an MST file, which - when I open it in Orca - looks just the part, has the right variables set to the right
values etc...
Unfortunately, when I try to apply it to an MSI file, I get a complaint that it's an invalid transform.
If I open the transform in Orca and save it again, it works, no problem. That additional step appears to add some binary headers to the file, from a quick side-by-side comparison.
So, what am I missing... Anyone?
Thanks,
Stuart
$signature = @'
using System.Text;
using System.Runtime.InteropServices;
using System;
using System.Collections.Generic;
public static class Msi
{
[DllImport("msi.dll", SetLastError = true)]
private static extern uint MsiOpenDatabase(string szDatabasePath, IntPtr phPersist, out IntPtr phDatabase);
[DllImport("msi.dll", CharSet = CharSet.Unicode)]
private static extern uint MsiDatabaseOpenView(IntPtr hDatabase, [MarshalAs(UnmanagedType.LPWStr)] string szQuery, out IntPtr phView);
[DllImport("msi.dll", CharSet = CharSet.Unicode)]
private static extern uint MsiViewFetch(IntPtr hView, out IntPtr hRecord);
[DllImport("msi.dll", CharSet = CharSet.Unicode)]
private static extern uint MsiViewExecute(IntPtr hView, IntPtr hRecord);
[DllImport("msi.dll", CharSet = CharSet.Unicode)]
private static extern uint MsiRecordGetString(IntPtr hRecord, int iField, System.Text.StringBuilder szValueBuf, ref int pcchValueBuf);
[DllImport("msi.dll", CharSet = CharSet.Unicode)]
private static extern uint MsiRecordSetString(IntPtr hRecord, int iField, string szValue);
[DllImport("msi.dll", ExactSpelling = true)]
private static extern IntPtr MsiCreateRecord(uint cParams);
[DllImport("msi.dll", ExactSpelling = true)]
private static extern uint MsiViewModify(IntPtr hView, uint eModifyMode, IntPtr hRecord);
[DllImport("msi.dll", SetLastError = true)]
private static extern uint MsiDatabaseGenerateTransform(IntPtr hDatabase, IntPtr hDatabaseReference, string szTransformFile, uint iReserved1, uint iReserved2);
[DllImport("msi.dll", ExactSpelling = true)]
private static extern uint MsiCloseHandle(IntPtr hAny);
private static readonly Dictionary<string,string> replacements = new Dictionary<string,string>();
private static readonly Dictionary<string,string> additions = new Dictionary<string,string>();
private static readonly List<IntPtr> openHandles = new List<IntPtr>();
private static string tempFile = System.IO.Path.GetTempFileName();
public static void ClearReplacements()
{
replacements.Clear();
}
public static void AddReplacement(string key, string value)
{
replacements.Add(key, value);
}
public static void ClearAdditions()
{
additions.Clear();
}
public static void AddAddition(string key, string value)
{
additions.Add(key, value);
}
public static void CreateTransform(string msiPath, string mstPath)
{
IntPtr msiInputHandle;
IntPtr msiAlteredHandle;
IntPtr msiDatabaseView;
IntPtr currentRecord;
uint result;
bool shouldContinue = true;
System.IO.File.Delete(tempFile);
System.IO.File.Copy(msiPath, tempFile);
if ((result = MsiOpenDatabase(msiPath, (IntPtr)0, out msiInputHandle)) != 0)
{
CleanUp("ERROR MsiOpenDatabase " + result);
return;
}
openHandles.Add(msiInputHandle);
if ((result = MsiOpenDatabase(tempFile, (IntPtr)2, out msiAlteredHandle)) != 0)
{
CleanUp("ERROR MsiOpenDatabase " + result);
return;
}
openHandles.Add(msiAlteredHandle);
if ((result = MsiDatabaseOpenView(msiAlteredHandle, "SELECT Property, Value FROM Property", out msiDatabaseView)) != 0)
{
CleanUp("ERROR MsiDatabaseOpenView " + result);
return;
}
openHandles.Add(msiDatabaseView);
if ((result = MsiViewExecute(msiDatabaseView, IntPtr.Zero)) != 0)
{
CleanUp("ERROR MsiViewExecute " + result);
return;
}
while (shouldContinue)
{
result = MsiViewFetch(msiDatabaseView, out currentRecord);
if (result == 259)
{
shouldContinue = false;
}
else if (result == 0)
{
openHandles.Add(currentRecord);
StringBuilder builder = new System.Text.StringBuilder(256);
int count = builder.Capacity;
if ((result = MsiRecordGetString(currentRecord, 1, builder, ref count)) != 0)
{
CleanUp("ERROR MsiRecordGetString " + result);
return;
}
string key = builder.ToString().Trim();
if (replacements.ContainsKey(key))
{
if ((result = MsiRecordSetString(currentRecord, 2, replacements[key])) != 0)
{
CleanUp("ERROR MsiRecordSetString " + result);
return;
}
if ((result = MsiViewModify(msiDatabaseView, 2, currentRecord)) != 0)
{
CleanUp("ERROR MsiViewModify " + result);
return;
}
}
MsiCloseHandle(currentRecord);
openHandles.Remove(currentRecord);
}
else
{
CleanUp("ERROR MsiViewFetch " + result);
return;
}
}
foreach (KeyValuePair<string,string> item in additions)
{
IntPtr newRecord = MsiCreateRecord(2);
openHandles.Add(newRecord);
if ((result = MsiRecordSetString(newRecord, 1, item.Key)) != 0)
{
CleanUp("ERROR MsiRecordSetString " + result);
return;
}
if ((result = MsiRecordSetString(newRecord, 2, item.Value)) != 0)
{
CleanUp("ERROR MsiRecordSetString " + result);
return;
}
if ((result = MsiViewModify(msiDatabaseView, 1, newRecord)) != 0)
{
CleanUp("ERROR MsiViewModify " + result);
return;
}
MsiCloseHandle(newRecord);
openHandles.Remove(newRecord);
}
if ((result = MsiDatabaseGenerateTransform(msiAlteredHandle, msiInputHandle, mstPath, 0, 0)) != 0)
{
CleanUp("ERROR MsiDatabaseGenerateTransform " + result);
return;
}
CleanUp("OK Created Transform File " + mstPath);
}
private static void CleanUp(string message)
{
Console.WriteLine(message);
foreach(IntPtr handle in openHandles)
{
MsiCloseHandle(handle);
}
openHandles.Clear();
System.IO.File.Delete(tempFile);
}
}
'@
Add-Type -TypeDefinition $signature
function GenerateMST(
[string]$MsiPath,
[string]$MstPath,
$Replacements,
$Additions
)
{
[Msi]::ClearReplacements()
[Msi]::ClearAdditions()
foreach($item in $replacements)
{
[Msi]::AddReplacement($item.Property, $item.Value);
}
foreach($item in $additions)
{
[Msi]::AddAddition($item.Property, $item.Value);
}
[Msi]::CreateTransform($msiPath, $mstPath)
}