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.
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.
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 = "
;[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 }