Automating MSI Changes - How not to have to use Orca each time you do a release

As I noted in an earlier post, I am a large non-fan of InstallShield for a number of reasons, which can be boiled down to it being a problem riddled overly expensive exercise in frustration.  I guess in their defense it could be said that I'm not part of their primary market, as I develop software I have to provide installers for, I'm not a release engineer per se.  But I was a customer once upon a time, and I deeply regretted it.

For those of us that use the tools that come with our development environment, this means we use Visual Studio deployment projects and then hack them up with Orca to work around all the things that Visual Studio won't let us do.  The Visual Studio deployment projects are fairly simple minded, they may creating an easy installer easy, but they basically aren't capable of supporting a more complex installer.  Some examples of things that Visual Studio can't support are modifications to the environment path, and use of custom serial number validation logic.

Most of us who use the deployment projects are familiar with Orca, a tool that allows for direct inspection and manipulation of the data tables in an MSI installer project.  Our normal course of action is to create a deployment project that does the basic work, and then before we we make a release, we crack open the MSI file produced by the deployment project and use Orca to make all the changes the deployment project won't support.  It's a recipe for disaster when dealing with repeated releases of a project, especially if the tasks we're performing in Orca are complex.  We make a mistake, bollix up the installer file, and then scramble to recover from the bullet hole in our feet.

What we really need to be able to do is to automate the changes we make with Orca.  The title of this blog posting is a bit of a misnomer, as we aren't really automating Orca, we're automating tasks we used to perform with Orca.  We use Orca to explore the changes we want to make, learn how to make them correctly, and can then use that knowledge to create an automated tool to make the changes for us.

The MSI system is built on top of a table based architecture that uses (sorta) SQL to manipulate the tables.  Automating changes to an MSI file primarily consists of applying SQL statements to the MSI file.  For example, here's a format string that's used to insert environment table settings into the MSI file;

INSERT INTO `Environment` ( `Environment`,`Name`,`Value`,`Component_`) VALUES('{0}','{1}','{2}','{3}')

All of the neccessary services are built in to the MSI support that's on all recently updated versions of Windows.  As I develop in .Net when I can, there's a great project on sourceforge that provides an excellent .net object model for using the MSI functions.  It's called the Pahvant.MSI.Interop package.  If you're using .Net and this isn't already in your toolbox, take a moment and go get it.

Now, I learned all about this from the Musical Nerdery blog, specifically this page.  I won't bother with much of an overview or a tutorial here, as this page does a bang up job.  However, once I read this, I did have to spend more time scrambling around to figure out how to put all the pieces together.  Hence, my contribution.  Here's a console app that uses the MSI interop package to;

  • Set the environment path variable
  • Insert a custom binary
  • Alter the ControlEvent table to automatically use a custom serial number validator

In the next post, I'll show how to use MSBuild to completely automate the process, so you've got a single command that builds the MSI and then runs this tool against it.

 

    1 using System;

    2 using System.Collections.Generic;

    3 using System.Text;

    4 using Pahvant.MSI;

    5 

    6 namespace TEEMSIFixup

    7 {

    8     class Program

    9     {

   10         /// <summary>

   11         /// Hack the MSI installer produced by the VS deployment project to

   12         ///    1) Insert an action to update the enviroment path so the C++ core DLLS are found

   13         ///    2) Install the custom serial number validation code and bind it into the operating flow

   14         ///        a)  shove the raw binary in

   15         ///        b)  define the custom action

   16         ///        c)  update the customer info records in the controlevent table

   17         /// </summary>

   18         /// <param name="args">The args.</param>

   19         static void Main(string[] args)

   20         {

   21             string dbfile = args[0];

   22             string qDLL = args[1];

   23             Database msi = new Database(dbfile, Database.OpenMode.Transact);

   24 

   25             string componentId = GetComponentId(msi);

   26             InsertV3DLLPath(msi, componentId);

   27             InsertQuestorBinary(msi, qDLL);

   28             List<Record> cinfo = GetCustomerInfoRecords(msi);

   29             UpdateCustomerInfoRecords(msi, cinfo);

   30             InsertSerialNoCheck(msi);

   31             ChangeControlEventCondition(msi);

   32             msi.Commit();

   33             msi.Dispose();

   34         }

   35         /// <summary>

   36         /// Gets the component id of the TEE2CSPI dll -  this DLL is part of the common library linked to the V3Core

   37         /// and is therefore an excellent ID to use for installer elements requireing a non-optional component id

   38         /// </summary>

   39         /// <remarks>

   40         /// Make this a configurable parameter, i.e. a command line item

   41         /// </remarks>

   42         /// <param name="msi">Reference to opened MSI database</param>

   43         /// <returns>string representation of component id</returns>

   44         static string GetComponentId(Database msi)

   45         {

   46             string sql =

   47                 "SELECT `Component_` FROM `File` WHERE `FileName`='TEE2CSPI.DLL|TEE2CSPI.dll'";

   48                 //"SELECT Component_ FROM FILE WHERE FileName=TEE2CSPI.DLL|TEE2CSPI.dll";

   49             View vw;

   50             using (vw = msi.OpenView(sql))

   51             {

   52                 vw.Execute();

   53                 Pahvant.MSI.Record rcd = vw.Fetch();

   54                 vw.Close();

   55                 return rcd.GetString(1);

   56             } 

   57 

   58         }

   59         /// <summary>

   60         /// Gets the customer info records from the ControlEvent table - these records are where the serial number validation must

   61         /// be performed

   62         /// </summary>

   63         /// <param name="msi">Reference to opened MSI database</param>

   64         /// <returns>List of located record instances</returns>

   65         static List<Record> GetCustomerInfoRecords(Database msi)

   66         {

   67             string sql =

   68                "SELECT `Dialog_`,`Control_`,`Event`,`Argument`,`Condition`,`Ordering` FROM `ControlEvent` WHERE `Dialog_`='CustomerInfoForm' AND `Control_`='NextButton'";

   69             View vw;

   70             List<Record> records = new List<Record>();

   71             using (vw = msi.OpenView(sql))

   72             {

   73                 vw.Execute();

   74                 Record rcd;

   75                 while ((rcd = vw.Fetch()) != null)

   76                     records.Add(rcd);

   77                 vw.Close();

   78                 return records;

   79             } 

   80 

   81         }

   82         /// <summary>

   83         /// Updates the customer info records, by reordering them from base 0 to base 1 - this allows a new record (the real serial number

   84         /// validator) to be inserted as the first step

   85         /// </summary>

   86         /// <param name="msi">Reference to opened MSI database</param>

   87         /// <param name="theRecords">List of located record instances</param>

   88         static void UpdateCustomerInfoRecords(Database msi,List<Record> theRecords)

   89         {

   90             string sql =

   91                 "UPDATE `ControlEvent` SET `Ordering` = '{1}' WHERE `Dialog_`='CustomerInfoForm' AND `Control_`='NextButton' AND `Ordering`={0}";

   92             string ssql;

   93             foreach(Record rcd in theRecords)

   94             {

   95                 int oldOrder = rcd.GetInteger(6);

   96                 //string evt = rcd.GetString(3);

   97                 ssql = String.Format(sql, oldOrder, oldOrder + 1);

   98                 MSIRunSQL(ssql, msi);

   99             }

  100         }

  101         /// <summary>

  102         /// Changes the control event condition in the Customer info record for the new dialog transition by adding

  103         /// a check to endure that PIDCHECK (set by the custom binary) is true -  this ensures that the installer will

  104         /// only go to the next installer page when a valid serial number has been entered

  105         /// </summary>

  106         /// <param name="msi">Reference to opened MSI database.</param>

  107         static void ChangeControlEventCondition(Database msi)

  108         {

  109             string sqlcond = "WHERE `Dialog_`='CustomerInfoForm' AND `Control_`='NextButton' AND `Event`='NewDialog'";

  110             string sql =

  111                "SELECT `Dialog_`,`Control_`,`Event`,`Argument`,`Condition`,`Ordering` FROM `ControlEvent` " + sqlcond;

  112             View vw;

  113             Record rcd;

  114             using (vw = msi.OpenView(sql))

  115             {

  116                 vw.Execute();

  117                 rcd = vw.Fetch();

  118                 vw.Close();

  119             }

  120             // delete this record

  121             string sqldel = "DELETE FROM `ControlEvent` " + sqlcond;

  122             MSIRunSQL(sqldel, msi);

  123             // create the new condition

  124             string arg = rcd.GetString(4);

  125             string cnd = rcd.GetString(5);

  126             int ord = rcd.GetInteger(6);

  127             cnd = "(PIDCHECK=\"TRUE\") AND " + cnd;

  128             string sqlFmt =

  129                 "INSERT INTO `ControlEvent` (`Dialog_`,`Control_`,`Event`,`Argument`,`Condition`,`Ordering`) VALUES('{0}','{1}','{2}','{3}','{4}',{5})";

  130             string insSql = String.Format(sqlFmt, "CustomerInfoForm", "NextButton", "NewDialog", arg, cnd, ord);

  131             MSIRunSQL(insSql, msi);

  132 

  133         }

  134         /// <summary>

  135         /// Inserts the v3 DLL path into the system path variable

  136         /// </summary>

  137         /// <param name="msi">Reference to opened MSI database.</param>

  138         /// <param name="component">Component ID to use when inserting the record.</param>

  139         /// <remarks>

  140         /// This could be further generalized to allow for the insertion of arbitrary path data

  141         /// </remarks>

  142         static void InsertV3DLLPath(Database msi,string component)

  143         {

  144             string sqlFmt =

  145                 "INSERT INTO `Environment` ( `Environment`,`Name`,`Value`,`Component_`) VALUES('{0}','{1}','{2}','{3}')";

  146             string env = "SETV3COREPATH";

  147             string nm = "*+PATH";

  148             string val = "Movie [~];[ProgramFilesFolder]Vibrant3d\v3d-os";

  149             string sql = String.Format(sqlFmt, env, nm, val, component);

  150             MSIRunSQL(sql, msi);           

  151 

  152         }

  153         /// <summary>

  154         /// Inserts the serial number check as the first step in the CustomerInfoForm exit flow - i.e. this is the new customer info record

  155         /// mentioned above.  This will call the custom action to validate the serial number, the MSI variable PIDCHECK will be set to true

  156         /// if the action succeeds and false if it does not

  157         /// </summary>

  158         /// <param name="msi">Reference to opened MSI database.</param>

  159         static void InsertSerialNoCheck(Database msi)

  160         {

  161             string sqlFmt =

  162                 "INSERT INTO `ControlEvent` ( `Dialog_`,`Control_`,`Event`,`Argument`,`Condition`,`Ordering`) VALUES('{0}','{1}','{2}','{3}','{4}',{5})";

  163             string dlg = "CustomerInfoForm";

  164             string ctl = "NextButton";

  165             string evt = "DoAction";

  166             string arg = "DoVerifyPID";

  167             string cnd = "CustomerInfoForm_ShowSerial=\"1\"";

  168             string ord = "1";

  169             string sql = String.Format(sqlFmt, dlg, ctl, evt, arg, cnd, ord);

  170             MSIRunSQL(sql, msi);

  171         }

  172         /// <summary>

  173         /// Inserts the questor binary into the binary table

  174         /// </summary>

  175         /// <remarks>

  176         /// This has already been generalized to insert pretty much anything so why is it still referencing the questor binary

  177         /// </remarks>

  178         /// <param name="msi">Reference to opened MSI database.</param>

  179         /// <param name="qDLL">The path to the DLL to be inserted</param>

  180         static void InsertQuestorBinary(Database msi,string qDLL)

  181         {

  182             Record rcd = new Record(1);

  183             rcd.SetStream(1, qDLL);

  184             MSIRunSQL("INSERT INTO `Binary` (`Name`, `Data`) VALUES ('QuestorDLL', ?)", msi, rcd);

  185             rcd.Dispose();

  186             MSIRunSQL(

  187                 "INSERT INTO `CustomAction` (`Action`, `Type`, `Source`, `Target`) VALUES('DoVerifyPID', '1', 'QuestorDLL', '_VerifyPID@4')",

  188                 msi);

  189         }

  190         /// <summary>

  191         /// Runs the given SQL statement against MSI database - this overload deals with situations

  192         /// where there is no associated record

  193         /// </summary>

  194         /// <param name="sSQL">The SQL statement to execute</param>

  195         /// <param name="msi">Reference to opened MSI database.</param>

  196         static void MSIRunSQL(string sSQL, Database msi)

  197         {

  198             MSIRunSQL(sSQL, msi, null);

  199         }

  200         /// <summary>

  201         /// Runs the given SQL statement against MSI database

  202         /// </summary>

  203         /// <param name="sSQL">The SQL statement to execute</param>

  204         /// <param name="msi">Reference to opened MSI database.</param>

  205         /// <param name="rcd">Record containing data to be written</param>

  206         static void MSIRunSQL(string sSQL, Database msi, Record rcd)

  207         {

  208             View vw;

  209             using(vw = msi.OpenView(sSQL))

  210             {

  211                 if(rcd == null)

  212                     vw.Execute();

  213                 else

  214                     vw.Execute(rcd);

  215             } 

  216         }

  217     }

  218 }

 

 

Published Sunday, September 24, 2006 10:39 AM by MarkMMullin
Filed Under:

Comments