main
cs 444 lines 20.3 KB
Raw
1 // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3 namespace WixToolset.Core.WindowsInstaller.Bind
4 {
5 using System;
6 using System.Globalization;
7 using System.IO;
8 using WixToolset.Core.Native.Msi;
9 using WixToolset.Data;
10 using WixToolset.Data.Symbols;
11 using WixToolset.Data.WindowsInstaller;
12 using WixToolset.Extensibility.Services;
13
14 internal class BindTransformCommand
15 {
16 public BindTransformCommand(IMessaging messaging, IBackendHelper backendHelper, IFileSystem fileSystem, FileSystemManager fileSystemManager, string intermediateFolder, WindowsInstallerData transform, string outputPath, TableDefinitionCollection tableDefinitions)
17 {
18 this.Messaging = messaging;
19 this.BackendHelper = backendHelper;
20 this.FileSystem = fileSystem;
21 this.FileSystemManager = fileSystemManager;
22 this.IntermediateFolder = intermediateFolder;
23 this.Transform = transform;
24 this.OutputPath = outputPath;
25 this.TableDefinitions = tableDefinitions;
26 }
27
28 private IMessaging Messaging { get; }
29
30 private IBackendHelper BackendHelper { get; }
31
32 private IFileSystem FileSystem { get; }
33
34 private FileSystemManager FileSystemManager { get; }
35
36 private TableDefinitionCollection TableDefinitions { get; }
37
38 private string IntermediateFolder { get; }
39
40 private WindowsInstallerData Transform { get; }
41
42 private string OutputPath { get; }
43
44 public void Execute()
45 {
46 var transformFlags = 0;
47
48 var targetOutput = new WindowsInstallerData(null);
49 var updatedOutput = new WindowsInstallerData(null);
50
51 // TODO: handle added columns
52
53 // to generate a localized transform, both the target and updated
54 // databases need to have the same code page. the only reason to
55 // set different code pages is to support localized primary key
56 // columns, but that would only support deleting rows. if this
57 // becomes necessary, define a PreviousCodepage property on the
58 // Output class and persist this throughout transform generation.
59 targetOutput.Codepage = this.Transform.Codepage;
60 updatedOutput.Codepage = this.Transform.Codepage;
61
62 // remove certain Property rows which will be populated from summary information values
63 string targetUpgradeCode = null;
64 string updatedUpgradeCode = null;
65
66 if (this.Transform.TryGetTable("Property", out var propertyTable))
67 {
68 for (var i = propertyTable.Rows.Count - 1; i >= 0; i--)
69 {
70 var row = propertyTable.Rows[i];
71 var id = row.FieldAsString(0);
72
73 if ("ProductCode" == id || "ProductLanguage" == id || "ProductVersion" == id)
74 {
75 propertyTable.Rows.RemoveAt(i);
76 }
77 else if ("UpgradeCode" == id)
78 {
79 updatedUpgradeCode = row.FieldAsString(1);
80 propertyTable.Rows.RemoveAt(i);
81 }
82 }
83 }
84
85 var targetSummaryInfo = targetOutput.EnsureTable(this.TableDefinitions["_SummaryInformation"]);
86 var updatedSummaryInfo = updatedOutput.EnsureTable(this.TableDefinitions["_SummaryInformation"]);
87 var targetPropertyTable = targetOutput.EnsureTable(this.TableDefinitions["Property"]);
88 var updatedPropertyTable = updatedOutput.EnsureTable(this.TableDefinitions["Property"]);
89
90 // process special summary information values
91 foreach (var row in this.Transform.Tables["_SummaryInformation"].Rows)
92 {
93 var summaryId = row.FieldAsInteger(0);
94 var summaryData = row.FieldAsString(1);
95
96 if ((int)SummaryInformation.Transform.CodePage == summaryId)
97 {
98 // convert from a web name if provided
99 var codePage = summaryData;
100 if (null == codePage)
101 {
102 codePage = "0";
103 }
104 else
105 {
106 codePage = this.BackendHelper.GetValidCodePage(codePage).ToString(CultureInfo.InvariantCulture);
107 }
108
109 var previousCodePage = row.Fields[1].PreviousData;
110 if (null == previousCodePage)
111 {
112 previousCodePage = "0";
113 }
114 else
115 {
116 previousCodePage = this.BackendHelper.GetValidCodePage(previousCodePage).ToString(CultureInfo.InvariantCulture);
117 }
118
119 var targetCodePageRow = targetSummaryInfo.CreateRow(null);
120 targetCodePageRow[0] = 1; // PID_CODEPAGE
121 targetCodePageRow[1] = previousCodePage;
122
123 var updatedCodePageRow = updatedSummaryInfo.CreateRow(null);
124 updatedCodePageRow[0] = 1; // PID_CODEPAGE
125 updatedCodePageRow[1] = codePage;
126 }
127 else if ((int)SummaryInformation.Transform.TargetPlatformAndLanguage == summaryId ||
128 (int)SummaryInformation.Transform.UpdatedPlatformAndLanguage == summaryId)
129 {
130 // the target language
131 var propertyData = summaryData.Split(';');
132 var lang = 2 == propertyData.Length ? propertyData[1] : "0";
133
134 var tempSummaryInfo = (int)SummaryInformation.Transform.TargetPlatformAndLanguage == summaryId ? targetSummaryInfo : updatedSummaryInfo;
135 var tempPropertyTable = (int)SummaryInformation.Transform.TargetPlatformAndLanguage == summaryId ? targetPropertyTable : updatedPropertyTable;
136
137 var productLanguageRow = tempPropertyTable.CreateRow(null);
138 productLanguageRow[0] = "ProductLanguage";
139 productLanguageRow[1] = lang;
140
141 // set the platform;language on the MSI to be generated
142 var templateRow = tempSummaryInfo.CreateRow(null);
143 templateRow[0] = 7; // PID_TEMPLATE
144 templateRow[1] = summaryData;
145 }
146 else if ((int)SummaryInformation.Transform.ProductCodes == summaryId)
147 {
148 var propertyData = summaryData.Split(';');
149
150 var targetProductCodeRow = targetPropertyTable.CreateRow(null);
151 targetProductCodeRow[0] = "ProductCode";
152 targetProductCodeRow[1] = propertyData[0].Substring(0, 38);
153
154 var targetProductVersionRow = targetPropertyTable.CreateRow(null);
155 targetProductVersionRow[0] = "ProductVersion";
156 targetProductVersionRow[1] = propertyData[0].Substring(38);
157
158 var updatedProductCodeRow = updatedPropertyTable.CreateRow(null);
159 updatedProductCodeRow[0] = "ProductCode";
160 updatedProductCodeRow[1] = propertyData[1].Substring(0, 38);
161
162 var updatedProductVersionRow = updatedPropertyTable.CreateRow(null);
163 updatedProductVersionRow[0] = "ProductVersion";
164 updatedProductVersionRow[1] = propertyData[1].Substring(38);
165
166 // UpgradeCode is optional and may not exists in the target
167 // or upgraded databases, so do not include a null-valued
168 // UpgradeCode property.
169
170 targetUpgradeCode = propertyData[2];
171 if (!String.IsNullOrEmpty(targetUpgradeCode))
172 {
173 var targetUpgradeCodeRow = targetPropertyTable.CreateRow(null);
174 targetUpgradeCodeRow[0] = "UpgradeCode";
175 targetUpgradeCodeRow[1] = targetUpgradeCode;
176
177 // If the target UpgradeCode is specified, an updated
178 // UpgradeCode is required.
179 if (String.IsNullOrEmpty(updatedUpgradeCode))
180 {
181 updatedUpgradeCode = targetUpgradeCode;
182 }
183 }
184
185 if (!String.IsNullOrEmpty(updatedUpgradeCode))
186 {
187 var updatedUpgradeCodeRow = updatedPropertyTable.CreateRow(null);
188 updatedUpgradeCodeRow[0] = "UpgradeCode";
189 updatedUpgradeCodeRow[1] = updatedUpgradeCode;
190 }
191 }
192 else if ((int)SummaryInformation.Transform.ValidationFlags == summaryId)
193 {
194 transformFlags = Convert.ToInt32(summaryData, CultureInfo.InvariantCulture);
195 }
196 else if ((int)SummaryInformation.Transform.Reserved11 == summaryId)
197 {
198 // PID_LASTPRINTED should be null for transforms
199 row.Operation = RowOperation.None;
200 }
201 else
202 {
203 // add everything else as is
204 var targetRow = targetSummaryInfo.CreateRow(null);
205 targetRow[0] = row[0];
206 targetRow[1] = row[1];
207
208 var updatedRow = updatedSummaryInfo.CreateRow(null);
209 updatedRow[0] = row[0];
210 updatedRow[1] = row[1];
211 }
212 }
213
214 // Validate that both databases have an UpgradeCode if the
215 // authoring transform will validate the UpgradeCode; otherwise,
216 // MsiCreateTransformSummaryinfo() will fail with 1620.
217 if (((int)TransformFlags.ValidateUpgradeCode & transformFlags) != 0 &&
218 (String.IsNullOrEmpty(targetUpgradeCode) || String.IsNullOrEmpty(updatedUpgradeCode)))
219 {
220 this.Messaging.Write(ErrorMessages.BothUpgradeCodesRequired());
221 }
222
223 string emptyFile = null;
224
225 foreach (var table in this.Transform.Tables)
226 {
227 // Ignore unreal tables when building transforms except the _Stream table.
228 // These tables are ignored when generating the database so there is no reason
229 // to process them here.
230 if (table.Definition.Unreal && "_Streams" != table.Name)
231 {
232 continue;
233 }
234
235 // process table operations
236 switch (table.Operation)
237 {
238 case TableOperation.Add:
239 updatedOutput.EnsureTable(table.Definition);
240 break;
241 case TableOperation.Drop:
242 targetOutput.EnsureTable(table.Definition);
243 continue;
244 default:
245 targetOutput.EnsureTable(table.Definition);
246 updatedOutput.EnsureTable(table.Definition);
247 break;
248 }
249
250 // process row operations
251 foreach (var row in table.Rows)
252 {
253 switch (row.Operation)
254 {
255 case RowOperation.Add:
256 var updatedTable = updatedOutput.EnsureTable(table.Definition);
257 updatedTable.Rows.Add(row);
258 continue;
259
260 case RowOperation.Delete:
261 var targetTable = targetOutput.EnsureTable(table.Definition);
262 targetTable.Rows.Add(row);
263
264 // fill-in non-primary key values
265 foreach (var field in row.Fields)
266 {
267 if (!field.Column.PrimaryKey)
268 {
269 if (ColumnType.Number == field.Column.Type && !field.Column.IsLocalizable)
270 {
271 field.Data = field.Column.MinValue ?? 0;
272 }
273 else if (ColumnType.Object == field.Column.Type)
274 {
275 if (null == emptyFile)
276 {
277 emptyFile = Path.Combine(this.IntermediateFolder, "empty");
278 }
279
280 field.Data = emptyFile;
281 }
282 else
283 {
284 field.Data = "0";
285 }
286 }
287 }
288 continue;
289 }
290
291 // Assure that the file table's sequence is populated
292 if ("File" == table.Name)
293 {
294 foreach (var fileRow in table.Rows)
295 {
296 if (null == fileRow[7])
297 {
298 if (RowOperation.Add == fileRow.Operation)
299 {
300 this.Messaging.Write(ErrorMessages.InvalidAddedFileRowWithoutSequence(fileRow.SourceLineNumbers, (string)fileRow[0]));
301 break;
302 }
303
304 // Set to 1 to prevent invalid IDT file from being generated
305 fileRow[7] = 1;
306 }
307 }
308 }
309
310 // process modified and unmodified rows
311 var modifiedRow = false;
312 var targetRow = table.Definition.CreateRow(null);
313 var updatedRow = row;
314 for (var i = 0; i < row.Fields.Length; i++)
315 {
316 var updatedField = row.Fields[i];
317
318 if (updatedField.Modified)
319 {
320 // set a different value in the target row to ensure this value will be modified during transform generation
321 if (ColumnType.Number == updatedField.Column.Type && !updatedField.Column.IsLocalizable)
322 {
323 var data = updatedField.AsNullableInteger();
324 targetRow[i] = (data == 1) ? 2 : 1;
325 }
326 else if (ColumnType.Object == updatedField.Column.Type)
327 {
328 if (null == emptyFile)
329 {
330 emptyFile = Path.Combine(this.IntermediateFolder, "empty");
331 }
332
333 targetRow[i] = emptyFile;
334 }
335 else
336 {
337 var data = updatedField.AsString();
338 targetRow[i] = (data == "0") ? "1" : "0";
339 }
340
341 modifiedRow = true;
342 }
343 else if (ColumnType.Object == updatedField.Column.Type)
344 {
345 var objectField = (ObjectField)updatedField;
346
347 // create an empty file for comparing against
348 if (null == objectField.PreviousData)
349 {
350 if (null == emptyFile)
351 {
352 emptyFile = Path.Combine(this.IntermediateFolder, "empty");
353 }
354
355 targetRow[i] = emptyFile;
356 modifiedRow = true;
357 }
358 else if (!this.FileSystemManager.CompareFiles(objectField.PreviousData, (string)objectField.Data))
359 {
360 targetRow[i] = objectField.PreviousData;
361 modifiedRow = true;
362 }
363 }
364 else // unmodified
365 {
366 if (null != updatedField.Data)
367 {
368 targetRow[i] = updatedField.Data;
369 }
370 }
371 }
372
373 // modified rows and certain special rows go in the target and updated msi databases
374 if (modifiedRow ||
375 ("Property" == table.Name &&
376 ("ProductCode" == (string)row[0] ||
377 "ProductLanguage" == (string)row[0] ||
378 "ProductVersion" == (string)row[0] ||
379 "UpgradeCode" == (string)row[0])))
380 {
381 var targetTable = targetOutput.EnsureTable(table.Definition);
382 targetTable.Rows.Add(targetRow);
383
384 var updatedTable = updatedOutput.EnsureTable(table.Definition);
385 updatedTable.Rows.Add(updatedRow);
386 }
387 }
388 }
389
390 // Any errors encountered up to this point can cause errors during generation.
391 if (this.Messaging.EncounteredError)
392 {
393 return;
394 }
395
396 var transformFileName = Path.GetFileNameWithoutExtension(this.OutputPath);
397 var targetDatabaseFile = Path.Combine(this.IntermediateFolder, String.Concat(transformFileName, "_target.msi"));
398 var updatedDatabaseFile = Path.Combine(this.IntermediateFolder, String.Concat(transformFileName, "_updated.msi"));
399
400 try
401 {
402 if (!String.IsNullOrEmpty(emptyFile))
403 {
404 using (var fileStream = this.FileSystem.OpenFile(null, emptyFile, FileMode.Create, FileAccess.Write, FileShare.None))
405 {
406 }
407 }
408
409 this.GenerateDatabase(targetOutput, targetDatabaseFile, keepAddedColumns: false);
410 this.GenerateDatabase(updatedOutput, updatedDatabaseFile, keepAddedColumns: true);
411
412 // make sure the directory exists
413 Directory.CreateDirectory(Path.GetDirectoryName(this.OutputPath));
414
415 // create the transform file
416 using (var targetDatabase = new Database(targetDatabaseFile, OpenDatabase.ReadOnly))
417 using (var updatedDatabase = new Database(updatedDatabaseFile, OpenDatabase.ReadOnly))
418 {
419 if (updatedDatabase.GenerateTransform(targetDatabase, this.OutputPath))
420 {
421 updatedDatabase.CreateTransformSummaryInfo(targetDatabase, this.OutputPath, (TransformErrorConditions)(transformFlags & 0xFFFF), (TransformValidations)((transformFlags >> 16) & 0xFFFF));
422 }
423 else
424 {
425 this.Messaging.Write(ErrorMessages.NoDifferencesInTransform(this.Transform.SourceLineNumbers));
426 }
427 }
428 }
429 finally
430 {
431 if (!String.IsNullOrEmpty(emptyFile))
432 {
433 File.Delete(emptyFile);
434 }
435 }
436 }
437
438 private void GenerateDatabase(WindowsInstallerData output, string outputPath, bool keepAddedColumns)
439 {
440 var command = new GenerateDatabaseCommand(this.Messaging, this.BackendHelper, this.FileSystem, this.FileSystemManager, output, outputPath, this.TableDefinitions, this.IntermediateFolder, keepAddedColumns, suppressAddingValidationRows: true, useSubdirectory: true);
441 command.Execute();
442 }
443 }
444 }