aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRob Mensching <rob@firegiant.com>2022-03-05 11:09:13 -0800
committerRob Mensching <rob@firegiant.com>2022-03-14 12:50:55 -0700
commit91cd2d65121a163e625d2f029025123b0f8467d2 (patch)
treef5684229d7971595d6f457913a988c1b81139ceb
parent8b72be6a36104497a0fe1606c47fb625b097972c (diff)
downloadwix-91cd2d65121a163e625d2f029025123b0f8467d2.tar.gz
wix-91cd2d65121a163e625d2f029025123b0f8467d2.tar.bz2
wix-91cd2d65121a163e625d2f029025123b0f8467d2.zip
Implement "wix msi transform"
Brings the functionality of torch into the WindowsInstallerBackend as the "transform" subcommand. Fixes 4602
-rw-r--r--src/wix/WixToolset.Core.WindowsInstaller/Bind/BindTransformCommand.cs22
-rw-r--r--src/wix/WixToolset.Core.WindowsInstaller/Bind/LoadTableDefinitionsCommand.cs6
-rw-r--r--src/wix/WixToolset.Core.WindowsInstaller/CommandLine/TransformSubcommand.cs352
-rw-r--r--src/wix/WixToolset.Core.WindowsInstaller/CommandLine/WindowsInstallerCommand.cs5
-rw-r--r--src/wix/WixToolset.Core.WindowsInstaller/Differ.cs178
-rw-r--r--src/wix/WixToolset.Core.WindowsInstaller/MsiBackend.cs4
-rw-r--r--src/wix/WixToolset.Core.WindowsInstaller/MsmBackend.cs4
-rw-r--r--src/wix/WixToolset.Core.WindowsInstaller/MspBackend.cs11
-rw-r--r--src/wix/WixToolset.Core.WindowsInstaller/MstBackend.cs39
-rw-r--r--src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindMsiOrMsmCommand.cs62
-rw-r--r--src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindTransformCommand.cs (renamed from src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindTranformCommand.cs)2
-rw-r--r--src/wix/WixToolset.Core.WindowsInstaller/WindowsInstallerBackendErrors.cs13
-rw-r--r--src/wix/WixToolset.Core.WindowsInstaller/WindowsInstallerBackendFactory.cs5
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/Language/Package.en-us.wxl4
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/Language/Package.ja-jp.wxl4
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TransformFixture.cs143
16 files changed, 653 insertions, 201 deletions
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Bind/BindTransformCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/Bind/BindTransformCommand.cs
index 3379ec5d..3d8e7595 100644
--- a/src/wix/WixToolset.Core.WindowsInstaller/Bind/BindTransformCommand.cs
+++ b/src/wix/WixToolset.Core.WindowsInstaller/Bind/BindTransformCommand.cs
@@ -62,18 +62,19 @@ namespace WixToolset.Core.WindowsInstaller.Bind
62 62
63 if (this.Transform.TryGetTable("Property", out var propertyTable)) 63 if (this.Transform.TryGetTable("Property", out var propertyTable))
64 { 64 {
65 for (int i = propertyTable.Rows.Count - 1; i >= 0; i--) 65 for (var i = propertyTable.Rows.Count - 1; i >= 0; i--)
66 { 66 {
67 Row row = propertyTable.Rows[i]; 67 var row = propertyTable.Rows[i];
68 var id = row.FieldAsString(0);
68 69
69 if ("ProductCode" == (string)row[0] || "ProductLanguage" == (string)row[0] || "ProductVersion" == (string)row[0] || "UpgradeCode" == (string)row[0]) 70 if ("ProductCode" == id || "ProductLanguage" == id || "ProductVersion" == id)
70 { 71 {
71 propertyTable.Rows.RemoveAt(i); 72 propertyTable.Rows.RemoveAt(i);
72 73 }
73 if ("UpgradeCode" == (string)row[0]) 74 else if ("UpgradeCode" == id)
74 { 75 {
75 updatedUpgradeCode = (string)row[1]; 76 updatedUpgradeCode = id;
76 } 77 propertyTable.Rows.RemoveAt(i);
77 } 78 }
78 } 79 }
79 } 80 }
@@ -383,11 +384,6 @@ namespace WixToolset.Core.WindowsInstaller.Bind
383 } 384 }
384 } 385 }
385 386
386 //foreach (BinderExtension extension in this.Extensions)
387 //{
388 // extension.PostBind(this.Context);
389 //}
390
391 // Any errors encountered up to this point can cause errors during generation. 387 // Any errors encountered up to this point can cause errors during generation.
392 if (this.Messaging.EncounteredError) 388 if (this.Messaging.EncounteredError)
393 { 389 {
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Bind/LoadTableDefinitionsCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/Bind/LoadTableDefinitionsCommand.cs
index 2eb95bc5..475a88f9 100644
--- a/src/wix/WixToolset.Core.WindowsInstaller/Bind/LoadTableDefinitionsCommand.cs
+++ b/src/wix/WixToolset.Core.WindowsInstaller/Bind/LoadTableDefinitionsCommand.cs
@@ -2,9 +2,7 @@
2 2
3namespace WixToolset.Core.WindowsInstaller.Bind 3namespace WixToolset.Core.WindowsInstaller.Bind
4{ 4{
5 using System;
6 using System.Collections.Generic; 5 using System.Collections.Generic;
7 using System.Globalization;
8 using System.Linq; 6 using System.Linq;
9 using WixToolset.Data; 7 using WixToolset.Data;
10 using WixToolset.Data.Symbols; 8 using WixToolset.Data.Symbols;
@@ -32,9 +30,9 @@ namespace WixToolset.Core.WindowsInstaller.Bind
32 public TableDefinitionCollection Execute() 30 public TableDefinitionCollection Execute()
33 { 31 {
34 var tableDefinitions = new TableDefinitionCollection(WindowsInstallerTableDefinitions.All); 32 var tableDefinitions = new TableDefinitionCollection(WindowsInstallerTableDefinitions.All);
35 var customColumnsById = this.Section.Symbols.OfType<WixCustomTableColumnSymbol>().ToDictionary(t => t.Id.Id); 33 var customColumnsById = this.Section?.Symbols.OfType<WixCustomTableColumnSymbol>().ToDictionary(t => t.Id.Id);
36 34
37 if (customColumnsById.Any()) 35 if (customColumnsById?.Any() == true)
38 { 36 {
39 foreach (var symbol in this.Section.Symbols.OfType<WixCustomTableSymbol>()) 37 foreach (var symbol in this.Section.Symbols.OfType<WixCustomTableSymbol>())
40 { 38 {
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/CommandLine/TransformSubcommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/CommandLine/TransformSubcommand.cs
new file mode 100644
index 00000000..7ed41d1a
--- /dev/null
+++ b/src/wix/WixToolset.Core.WindowsInstaller/CommandLine/TransformSubcommand.cs
@@ -0,0 +1,352 @@
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
3namespace WixToolset.Core.WindowsInstaller.CommandLine
4{
5 using System;
6 using System.IO;
7 using System.Threading;
8 using System.Threading.Tasks;
9 using WixToolset.Core.WindowsInstaller.Bind;
10 using WixToolset.Core.WindowsInstaller.Unbind;
11 using WixToolset.Data;
12 using WixToolset.Data.Symbols;
13 using WixToolset.Data.WindowsInstaller;
14 using WixToolset.Extensibility;
15 using WixToolset.Extensibility.Services;
16
17 internal class TransformSubcommand : WindowsInstallerSubcommandBase
18 {
19 public TransformSubcommand(IServiceProvider serviceProvider)
20 {
21 this.Messaging = serviceProvider.GetService<IMessaging>();
22 this.BackendHelper = serviceProvider.GetService<IBackendHelper>();
23 this.ExtensionManager = serviceProvider.GetService<IExtensionManager>();
24 }
25
26 private IMessaging Messaging { get; }
27
28 private IBackendHelper BackendHelper { get; }
29
30 private IExtensionManager ExtensionManager { get; }
31
32 private string OutputPath { get; set; }
33
34 private string TargetPath { get; set; }
35
36 private string UpdatedPath { get; set; }
37
38 private string ExportBasePath { get; set; }
39
40 private string IntermediateFolder { get; set; }
41
42 private bool IsAdminImage { get; set; }
43
44 private bool PreserveUnchangedRows { get; set; }
45
46 private bool ShowPedanticMessages { get; set; }
47
48 private bool SuppressKeepingSpecialRows { get; set; }
49
50 private bool OutputAsWixout { get; set; }
51
52 private TransformFlags ValidationFlags { get; set; }
53
54 public override Task<int> ExecuteAsync(CancellationToken cancellationToken)
55 {
56 if (String.IsNullOrEmpty(this.TargetPath))
57 {
58 Console.Error.WriteLine("Input file required");
59 return Task.FromResult(-1);
60 }
61
62 if (String.IsNullOrEmpty(this.OutputPath))
63 {
64 Console.Error.WriteLine("Output file required");
65 return Task.FromResult(-1);
66 }
67
68 if (String.IsNullOrEmpty(this.IntermediateFolder))
69 {
70 this.IntermediateFolder = Path.GetTempPath();
71 }
72
73 var transform = this.LoadTransform();
74
75 if (!this.Messaging.EncounteredError)
76 {
77 this.SaveTransform(transform);
78 }
79
80 return Task.FromResult(this.Messaging.EncounteredError ? 1 : 0);
81 }
82
83 public override bool TryParseArgument(ICommandLineParser parser, string argument)
84 {
85 if (parser.IsSwitch(argument))
86 {
87 var parameter = argument.Substring(1);
88 switch (parameter.ToLowerInvariant())
89 {
90 case "a":
91 this.IsAdminImage = true;
92 return true;
93
94 case "intermediatefolder":
95 this.IntermediateFolder = parser.GetNextArgumentAsDirectoryOrError(argument);
96 return true;
97
98 case "o":
99 case "out":
100 this.OutputPath = parser.GetNextArgumentAsFilePathOrError(argument);
101 return true;
102
103 case "p":
104 this.PreserveUnchangedRows = true;
105 return true;
106
107 case "pedantic":
108 this.ShowPedanticMessages = true;
109 return true;
110
111 case "serr":
112 {
113 var serr = parser.GetNextArgumentOrError(argument);
114
115 switch (serr.ToLowerInvariant())
116 {
117 case "a":
118 this.ValidationFlags |= TransformFlags.ErrorAddExistingRow;
119 return true;
120
121 case "b":
122 this.ValidationFlags |= TransformFlags.ErrorDeleteMissingRow;
123 return true;
124
125 case "c":
126 this.ValidationFlags |= TransformFlags.ErrorAddExistingTable;
127 return true;
128
129 case "d":
130 this.ValidationFlags |= TransformFlags.ErrorDeleteMissingTable;
131 return true;
132
133 case "e":
134 this.ValidationFlags |= TransformFlags.ErrorUpdateMissingRow;
135 return true;
136
137 case "f":
138 this.ValidationFlags |= TransformFlags.ErrorChangeCodePage;
139 return true;
140
141 default:
142 this.Messaging.Write(ErrorMessages.ExpectedArgument(serr));
143 return true;
144 }
145 }
146
147 case "val":
148 {
149 var val = parser.GetNextArgumentOrError(argument);
150
151 switch (val.ToLowerInvariant())
152 {
153 case "language":
154 this.ValidationFlags |= TransformFlags.LanguageTransformDefault;
155 return true;
156
157 case "instance":
158 this.ValidationFlags |= TransformFlags.InstanceTransformDefault;
159 return true;
160
161 case "patch":
162 this.ValidationFlags |= TransformFlags.PatchTransformDefault;
163 return true;
164
165 case "g":
166 this.ValidationFlags |= TransformFlags.ValidateUpgradeCode;
167 return true;
168
169 case "l":
170 this.ValidationFlags |= TransformFlags.ValidateLanguage;
171 return true;
172
173 case "r":
174 this.ValidationFlags |= TransformFlags.ValidateProduct;
175 return true;
176
177 case "s":
178 this.ValidationFlags |= TransformFlags.ValidateMajorVersion;
179 return true;
180
181 case "t":
182 this.ValidationFlags |= TransformFlags.ValidateMinorVersion;
183 return true;
184
185 case "u":
186 this.ValidationFlags |= TransformFlags.ValidateUpdateVersion;
187 return true;
188
189 case "v":
190 this.ValidationFlags |= TransformFlags.ValidateNewLessBaseVersion;
191 return true;
192
193 case "w":
194 this.ValidationFlags |= TransformFlags.ValidateNewLessEqualBaseVersion;
195 return true;
196
197 case "x":
198 this.ValidationFlags |= TransformFlags.ValidateNewEqualBaseVersion;
199 return true;
200
201 case "y":
202 this.ValidationFlags |= TransformFlags.ValidateNewGreaterEqualBaseVersion;
203 return true;
204
205 case "z":
206 this.ValidationFlags |= TransformFlags.ValidateNewGreaterBaseVersion;
207 return true;
208
209 default:
210 this.Messaging.Write(ErrorMessages.ExpectedArgument(val));
211 return true;
212 }
213 }
214
215 case "x":
216 this.ExportBasePath = parser.GetNextArgumentAsDirectoryOrError(argument);
217 return true;
218
219 case "xo":
220 this.OutputAsWixout = true;
221 return true;
222 }
223 }
224 else if (String.IsNullOrEmpty(this.TargetPath))
225 {
226 this.TargetPath = argument;
227 return true;
228 }
229 else if (String.IsNullOrEmpty(this.UpdatedPath))
230 {
231 this.UpdatedPath = argument;
232 return true;
233 }
234
235 return false;
236 }
237
238 private WindowsInstallerData LoadTransform()
239 {
240 WindowsInstallerData transform;
241
242 if (String.IsNullOrEmpty(this.UpdatedPath))
243 {
244 Exception exception;
245
246 (transform, exception) = LoadWindowsInstallerDataSafely(this.TargetPath);
247
248 if (transform?.Type != OutputType.Transform)
249 {
250 this.Messaging.Write(WindowsInstallerBackendErrors.CannotLoadWixoutAsTransform(new SourceLineNumber(this.TargetPath), exception));
251 }
252 }
253 else
254 {
255 transform = this.CreateTransform();
256
257 if (null == transform.Tables || 0 >= transform.Tables.Count)
258 {
259 this.Messaging.Write(ErrorMessages.NoDifferencesInTransform(new SourceLineNumber(this.OutputPath)));
260 }
261 }
262
263 return transform;
264 }
265
266 private void SaveTransform(WindowsInstallerData transform)
267 {
268 if (this.OutputAsWixout)
269 {
270 using (var output = WixOutput.Create(this.OutputPath))
271 {
272 transform.Save(output);
273 }
274 }
275 else
276 {
277 var fileSystemExtensions = this.ExtensionManager.GetServices<IFileSystemExtension>();
278 var fileSystemManager = new FileSystemManager(fileSystemExtensions);
279
280 var tableDefinitions = this.GetTableDefinitions();
281
282 var bindCommand = new BindTransformCommand(this.Messaging, this.BackendHelper, fileSystemManager, this.IntermediateFolder, transform, this.OutputPath, tableDefinitions);
283 bindCommand.Execute();
284 }
285 }
286
287 private WindowsInstallerData CreateTransform()
288 {
289 if (!TryLoadWindowsInstallerData(this.TargetPath, out var targetOutput))
290 {
291 var unbindCommand = new UnbindMsiOrMsmCommand(this.Messaging, this.BackendHelper, this.TargetPath, this.ExportBasePath, this.IntermediateFolder, this.IsAdminImage, suppressDemodularization: true, suppressExtractCabinets: true);
292 targetOutput = unbindCommand.Execute();
293 }
294
295 if (!TryLoadWindowsInstallerData(this.TargetPath, out var updatedOutput))
296 {
297 var unbindCommand = new UnbindMsiOrMsmCommand(this.Messaging, this.BackendHelper, this.UpdatedPath, this.ExportBasePath, this.IntermediateFolder, this.IsAdminImage, suppressDemodularization: true, suppressExtractCabinets: true);
298 updatedOutput = unbindCommand.Execute();
299 }
300
301 var differ = new Differ(this.Messaging)
302 {
303 PreserveUnchangedRows = this.PreserveUnchangedRows,
304 ShowPedanticMessages = this.ShowPedanticMessages,
305 SuppressKeepingSpecialRows = this.SuppressKeepingSpecialRows
306 };
307
308 return differ.Diff(targetOutput, updatedOutput, this.ValidationFlags);
309 }
310
311 private TableDefinitionCollection GetTableDefinitions()
312 {
313 var backendExtensions = this.ExtensionManager.GetServices<IWindowsInstallerBackendBinderExtension>();
314
315 var loadTableDefinitions = new LoadTableDefinitionsCommand(this.Messaging, null, backendExtensions);
316 return loadTableDefinitions.Execute();
317 }
318
319 private static bool TryLoadWindowsInstallerData(string path, out WindowsInstallerData data)
320 {
321 data = null;
322
323 var extension = Path.GetExtension(path);
324
325 // If the path is _not_ obviously a Windows Installer database, let's try opening it as
326 // our own data file format.
327 if (!extension.Equals(".msi", StringComparison.OrdinalIgnoreCase) && !extension.Equals(".msm", StringComparison.OrdinalIgnoreCase))
328 {
329 (data, _) = LoadWindowsInstallerDataSafely(path);
330 }
331
332 return data != null;
333 }
334
335 private static (WindowsInstallerData, Exception) LoadWindowsInstallerDataSafely(string path)
336 {
337 WindowsInstallerData data = null;
338 Exception exception = null;
339
340 try
341 {
342 data = WindowsInstallerData.Load(path);
343 }
344 catch (Exception e)
345 {
346 exception = e;
347 }
348
349 return (data, exception);
350 }
351 }
352}
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/CommandLine/WindowsInstallerCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/CommandLine/WindowsInstallerCommand.cs
index a0da7fa4..ed0c0658 100644
--- a/src/wix/WixToolset.Core.WindowsInstaller/CommandLine/WindowsInstallerCommand.cs
+++ b/src/wix/WixToolset.Core.WindowsInstaller/CommandLine/WindowsInstallerCommand.cs
@@ -49,6 +49,10 @@ namespace WixToolset.Core.WindowsInstaller.CommandLine
49 this.Subcommand = new InscribeSubcommand(this.ServiceProvider); 49 this.Subcommand = new InscribeSubcommand(this.ServiceProvider);
50 return true; 50 return true;
51 51
52 case "transform":
53 this.Subcommand = new TransformSubcommand(this.ServiceProvider);
54 return true;
55
52 case "validate": 56 case "validate":
53 this.Subcommand = new ValidateSubcommand(this.ServiceProvider); 57 this.Subcommand = new ValidateSubcommand(this.ServiceProvider);
54 return true; 58 return true;
@@ -72,6 +76,7 @@ namespace WixToolset.Core.WindowsInstaller.CommandLine
72 Console.WriteLine("Commands:"); 76 Console.WriteLine("Commands:");
73 Console.WriteLine(); 77 Console.WriteLine();
74 Console.WriteLine(" inscribe Updates MSI database with cabinet signature information."); 78 Console.WriteLine(" inscribe Updates MSI database with cabinet signature information.");
79 Console.WriteLine(" transform Creates an MST transform file.");
75 Console.WriteLine(" validate Validates MSI database using standard or custom ICEs."); 80 Console.WriteLine(" validate Validates MSI database using standard or custom ICEs.");
76 } 81 }
77 } 82 }
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Differ.cs b/src/wix/WixToolset.Core.WindowsInstaller/Differ.cs
index 304d0152..8b474605 100644
--- a/src/wix/WixToolset.Core.WindowsInstaller/Differ.cs
+++ b/src/wix/WixToolset.Core.WindowsInstaller/Differ.cs
@@ -1,18 +1,15 @@
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. 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 2
3#if DELETE
4
5namespace WixToolset.Core.WindowsInstaller 3namespace WixToolset.Core.WindowsInstaller
6{ 4{
7 using System; 5 using System;
8 using System.Collections; 6 using System.Collections;
9 using System.Collections.Generic; 7 using System.Collections.Generic;
10 using System.Globalization; 8 using System.Globalization;
11 using WixToolset.Core.WindowsInstaller.Msi; 9 using WixToolset.Core.Native.Msi;
12 using WixToolset.Data; 10 using WixToolset.Data;
13 using WixToolset.Data.Symbols; 11 using WixToolset.Data.Symbols;
14 using WixToolset.Data.WindowsInstaller; 12 using WixToolset.Data.WindowsInstaller;
15 using WixToolset.Data.WindowsInstaller.Rows;
16 using WixToolset.Extensibility; 13 using WixToolset.Extensibility;
17 using WixToolset.Extensibility.Services; 14 using WixToolset.Extensibility.Services;
18 15
@@ -22,9 +19,6 @@ namespace WixToolset.Core.WindowsInstaller
22 public sealed class Differ 19 public sealed class Differ
23 { 20 {
24 private readonly List<IInspectorExtension> inspectorExtensions; 21 private readonly List<IInspectorExtension> inspectorExtensions;
25 private bool showPedanticMessages;
26 private bool suppressKeepingSpecialRows;
27 private bool preserveUnchangedRows;
28 private const char sectionDelimiter = '/'; 22 private const char sectionDelimiter = '/';
29 private readonly IMessaging messaging; 23 private readonly IMessaging messaging;
30 private SummaryInformationStreams transformSummaryInfo; 24 private SummaryInformationStreams transformSummaryInfo;
@@ -42,31 +36,19 @@ namespace WixToolset.Core.WindowsInstaller
42 /// Gets or sets the option to show pedantic messages. 36 /// Gets or sets the option to show pedantic messages.
43 /// </summary> 37 /// </summary>
44 /// <value>The option to show pedantic messages.</value> 38 /// <value>The option to show pedantic messages.</value>
45 public bool ShowPedanticMessages 39 public bool ShowPedanticMessages { get; set; }
46 {
47 get { return this.showPedanticMessages; }
48 set { this.showPedanticMessages = value; }
49 }
50 40
51 /// <summary> 41 /// <summary>
52 /// Gets or sets the option to suppress keeping special rows. 42 /// Gets or sets the option to suppress keeping special rows.
53 /// </summary> 43 /// </summary>
54 /// <value>The option to suppress keeping special rows.</value> 44 /// <value>The option to suppress keeping special rows.</value>
55 public bool SuppressKeepingSpecialRows 45 public bool SuppressKeepingSpecialRows { get; set; }
56 {
57 get { return this.suppressKeepingSpecialRows; }
58 set { this.suppressKeepingSpecialRows = value; }
59 }
60 46
61 /// <summary> 47 /// <summary>
62 /// Gets or sets the flag to determine if all rows, even unchanged ones will be persisted in the output. 48 /// Gets or sets the flag to determine if all rows, even unchanged ones will be persisted in the output.
63 /// </summary> 49 /// </summary>
64 /// <value>The option to keep all rows including unchanged rows.</value> 50 /// <value>The option to keep all rows including unchanged rows.</value>
65 public bool PreserveUnchangedRows 51 public bool PreserveUnchangedRows { get; set; }
66 {
67 get { return this.preserveUnchangedRows; }
68 set { this.preserveUnchangedRows = value; }
69 }
70 52
71 /// <summary> 53 /// <summary>
72 /// Adds an extension. 54 /// Adds an extension.
@@ -97,7 +79,7 @@ namespace WixToolset.Core.WindowsInstaller
97 /// <returns>The transform.</returns> 79 /// <returns>The transform.</returns>
98 public WindowsInstallerData Diff(WindowsInstallerData targetOutput, WindowsInstallerData updatedOutput, TransformFlags validationFlags) 80 public WindowsInstallerData Diff(WindowsInstallerData targetOutput, WindowsInstallerData updatedOutput, TransformFlags validationFlags)
99 { 81 {
100 WindowsInstallerData transform = new WindowsInstallerData(null); 82 var transform = new WindowsInstallerData(null);
101 transform.Type = OutputType.Transform; 83 transform.Type = OutputType.Transform;
102 transform.Codepage = updatedOutput.Codepage; 84 transform.Codepage = updatedOutput.Codepage;
103 this.transformSummaryInfo = new SummaryInformationStreams(); 85 this.transformSummaryInfo = new SummaryInformationStreams();
@@ -119,34 +101,34 @@ namespace WixToolset.Core.WindowsInstaller
119 } 101 }
120 102
121 // compare the contents of the tables 103 // compare the contents of the tables
122 foreach (Table targetTable in targetOutput.Tables) 104 foreach (var targetTable in targetOutput.Tables)
123 { 105 {
124 Table updatedTable = updatedOutput.Tables[targetTable.Name]; 106 var updatedTable = updatedOutput.Tables[targetTable.Name];
125 TableOperation operation = TableOperation.None; 107 var operation = TableOperation.None;
126 108
127 List<Row> rows = this.CompareTables(targetOutput, targetTable, updatedTable, out operation); 109 var rows = this.CompareTables(targetOutput, targetTable, updatedTable, out operation);
128 110
129 if (TableOperation.Drop == operation) 111 if (TableOperation.Drop == operation)
130 { 112 {
131 Table droppedTable = transform.EnsureTable(targetTable.Definition); 113 var droppedTable = transform.EnsureTable(targetTable.Definition);
132 droppedTable.Operation = TableOperation.Drop; 114 droppedTable.Operation = TableOperation.Drop;
133 } 115 }
134 else if (TableOperation.None == operation) 116 else if (TableOperation.None == operation)
135 { 117 {
136 Table modified = transform.EnsureTable(updatedTable.Definition); 118 var modified = transform.EnsureTable(updatedTable.Definition);
137 rows.ForEach(r => modified.Rows.Add(r)); 119 rows.ForEach(r => modified.Rows.Add(r));
138 } 120 }
139 } 121 }
140 122
141 // added tables 123 // added tables
142 foreach (Table updatedTable in updatedOutput.Tables) 124 foreach (var updatedTable in updatedOutput.Tables)
143 { 125 {
144 if (null == targetOutput.Tables[updatedTable.Name]) 126 if (null == targetOutput.Tables[updatedTable.Name])
145 { 127 {
146 Table addedTable = transform.EnsureTable(updatedTable.Definition); 128 var addedTable = transform.EnsureTable(updatedTable.Definition);
147 addedTable.Operation = TableOperation.Add; 129 addedTable.Operation = TableOperation.Add;
148 130
149 foreach (Row updatedRow in updatedTable.Rows) 131 foreach (var updatedRow in updatedTable.Rows)
150 { 132 {
151 updatedRow.Operation = RowOperation.Add; 133 updatedRow.Operation = RowOperation.Add;
152 updatedRow.SectionId = sectionDelimiter + updatedRow.SectionId; 134 updatedRow.SectionId = sectionDelimiter + updatedRow.SectionId;
@@ -156,9 +138,9 @@ namespace WixToolset.Core.WindowsInstaller
156 } 138 }
157 139
158 // set summary information properties 140 // set summary information properties
159 if (!this.suppressKeepingSpecialRows) 141 if (!this.SuppressKeepingSpecialRows)
160 { 142 {
161 Table summaryInfoTable = transform.Tables["_SummaryInformation"]; 143 var summaryInfoTable = transform.Tables["_SummaryInformation"];
162 this.UpdateTransformSummaryInformationTable(summaryInfoTable, validationFlags); 144 this.UpdateTransformSummaryInformationTable(summaryInfoTable, validationFlags);
163 } 145 }
164 146
@@ -170,53 +152,56 @@ namespace WixToolset.Core.WindowsInstaller
170 /// </summary> 152 /// </summary>
171 /// <param name="index">The indexed rows.</param> 153 /// <param name="index">The indexed rows.</param>
172 /// <param name="row">The row to index.</param> 154 /// <param name="row">The row to index.</param>
173 private void AddIndexedRow(IDictionary index, Row row) 155 private void AddIndexedRow(IDictionary<string, Row> index, Row row)
174 { 156 {
175 string primaryKey = row.GetPrimaryKey('/'); 157 var primaryKey = row.GetPrimaryKey('/');
176 if (null != primaryKey) 158
159 // If there is no primary, use the string representation of the row as its
160 // primary key (even though it may not be unique).
161 if (String.IsNullOrEmpty(primaryKey))
177 { 162 {
178 // Overriding WixActionRows have a primary key defined and take precedence in the index. 163 // This is provided for compatibility with unreal tables with no primary key
179 if (row is WixActionRow) 164 // all real tables must specify at least one column as the primary key.
165 primaryKey = row.ToString();
166 index[primaryKey] = row;
167 }
168 else
169 {
170 if (!index.TryGetValue(primaryKey, out var existingRow))
171 {
172 index.Add(primaryKey, row);
173 }
174 else
180 { 175 {
181 WixActionRow currentRow = (WixActionRow)row; 176#if TODO
182 if (index.Contains(primaryKey)) 177 // Overriding WixActionRows have a primary key defined and take precedence in the index.
178 if (row is WixActionRow currentActionRow)
183 { 179 {
184 // If the current row is not overridable, see if the indexed row is. 180 // If the current row is not overridable, see if the indexed row is.
185 if (!currentRow.Overridable) 181 if (!currentActionRow.Overridable)
186 { 182 {
187 WixActionRow indexedRow = index[primaryKey] as WixActionRow; 183 if (existingRow is WixActionRow existingActionRow && existingActionRow.Overridable)
188 if (null != indexedRow && indexedRow.Overridable)
189 { 184 {
190 // The indexed key is overridable and should be replaced 185 // The indexed key is overridable and should be replaced
191 // (not removed and re-added which results in two Array.Copy 186 // (not removed and re-added which results in two Array.Copy
192 // operations for SortedList, or may be re-hashing in other 187 // operations for SortedList, or may be re-hashing in other
193 // implementations of IDictionary). 188 // implementations of IDictionary).
194 index[primaryKey] = currentRow; 189 index[primaryKey] = currentActionRow;
195 } 190 }
196 } 191 }
197 192
198 // If we got this far, the row does not need to be indexed. 193 // If we got this far, the row does not need to be indexed.
199 return; 194 return;
200 } 195 }
201 } 196#endif
202 197
203 // Nothing else should be added more than once. 198 // Nothing else should be added more than once.
204 if (!index.Contains(primaryKey)) 199 if (this.ShowPedanticMessages)
205 { 200 {
206 index.Add(primaryKey, row); 201 this.messaging.Write(ErrorMessages.DuplicatePrimaryKey(row.SourceLineNumbers, primaryKey, row.Table.Name));
207 } 202 }
208 else if (this.showPedanticMessages)
209 {
210 this.messaging.Write(ErrorMessages.DuplicatePrimaryKey(row.SourceLineNumbers, primaryKey, row.Table.Name));
211 } 203 }
212 } 204 }
213 else // use the string representation of the row as its primary key (it may not be unique)
214 {
215 // this is provided for compatibility with unreal tables with no primary key
216 // all real tables must specify at least one column as the primary key
217 primaryKey = row.ToString();
218 index[primaryKey] = row;
219 }
220 } 205 }
221 206
222 private Row CompareRows(Table targetTable, Row targetRow, Row updatedRow, out RowOperation operation, out bool keepRow) 207 private Row CompareRows(Table targetTable, Row targetRow, Row updatedRow, out RowOperation operation, out bool keepRow)
@@ -235,7 +220,7 @@ namespace WixToolset.Core.WindowsInstaller
235 else if (null == updatedRow) 220 else if (null == updatedRow)
236 { 221 {
237 operation = targetRow.Operation = RowOperation.Delete; 222 operation = targetRow.Operation = RowOperation.Delete;
238 targetRow.SectionId = targetRow.SectionId + sectionDelimiter; 223 targetRow.SectionId += sectionDelimiter;
239 comparedRow = targetRow; 224 comparedRow = targetRow;
240 keepRow = true; 225 keepRow = true;
241 } 226 }
@@ -243,7 +228,7 @@ namespace WixToolset.Core.WindowsInstaller
243 else // possibly modified 228 else // possibly modified
244 { 229 {
245 updatedRow.Operation = RowOperation.None; 230 updatedRow.Operation = RowOperation.None;
246 if (!this.suppressKeepingSpecialRows && "_SummaryInformation" == targetTable.Name) 231 if (!this.SuppressKeepingSpecialRows && "_SummaryInformation" == targetTable.Name)
247 { 232 {
248 // ignore rows that shouldn't be in a transform 233 // ignore rows that shouldn't be in a transform
249 if (Enum.IsDefined(typeof(SummaryInformation.Transform), (int)updatedRow[0])) 234 if (Enum.IsDefined(typeof(SummaryInformation.Transform), (int)updatedRow[0]))
@@ -256,18 +241,18 @@ namespace WixToolset.Core.WindowsInstaller
256 } 241 }
257 else 242 else
258 { 243 {
259 if (this.preserveUnchangedRows) 244 if (this.PreserveUnchangedRows)
260 { 245 {
261 keepRow = true; 246 keepRow = true;
262 } 247 }
263 248
264 for (int i = 0; i < updatedRow.Fields.Length; i++) 249 for (var i = 0; i < updatedRow.Fields.Length; i++)
265 { 250 {
266 ColumnDefinition columnDefinition = updatedRow.Fields[i].Column; 251 var columnDefinition = updatedRow.Fields[i].Column;
267 252
268 if (!columnDefinition.PrimaryKey) 253 if (!columnDefinition.PrimaryKey)
269 { 254 {
270 bool modified = false; 255 var modified = false;
271 256
272 if (i >= targetRow.Fields.Length) 257 if (i >= targetRow.Fields.Length)
273 { 258 {
@@ -290,12 +275,12 @@ namespace WixToolset.Core.WindowsInstaller
290 updatedRow.Fields[i].PreviousData = (string)targetRow.Fields[i].Data; 275 updatedRow.Fields[i].PreviousData = (string)targetRow.Fields[i].Data;
291 276
292 // keep rows containing preserved fields so the historical data is available to the binder 277 // keep rows containing preserved fields so the historical data is available to the binder
293 keepRow = !this.suppressKeepingSpecialRows; 278 keepRow = !this.SuppressKeepingSpecialRows;
294 } 279 }
295 else if (ColumnType.Object == columnDefinition.Type) 280 else if (ColumnType.Object == columnDefinition.Type)
296 { 281 {
297 ObjectField targetObjectField = (ObjectField)targetRow.Fields[i]; 282 var targetObjectField = (ObjectField)targetRow.Fields[i];
298 ObjectField updatedObjectField = (ObjectField)updatedRow.Fields[i]; 283 var updatedObjectField = (ObjectField)updatedRow.Fields[i];
299 284
300 updatedObjectField.PreviousEmbeddedFileIndex = targetObjectField.EmbeddedFileIndex; 285 updatedObjectField.PreviousEmbeddedFileIndex = targetObjectField.EmbeddedFileIndex;
301 updatedObjectField.PreviousBaseUri = targetObjectField.BaseUri; 286 updatedObjectField.PreviousBaseUri = targetObjectField.BaseUri;
@@ -308,7 +293,7 @@ namespace WixToolset.Core.WindowsInstaller
308 updatedObjectField.UnresolvedPreviousData = (string)targetObjectField.UnresolvedData; 293 updatedObjectField.UnresolvedPreviousData = (string)targetObjectField.UnresolvedData;
309 294
310 // keep rows containing object fields so the files can be compared in the binder 295 // keep rows containing object fields so the files can be compared in the binder
311 keepRow = !this.suppressKeepingSpecialRows; 296 keepRow = !this.SuppressKeepingSpecialRows;
312 } 297 }
313 else 298 else
314 { 299 {
@@ -342,7 +327,7 @@ namespace WixToolset.Core.WindowsInstaller
342 327
343 private List<Row> CompareTables(WindowsInstallerData targetOutput, Table targetTable, Table updatedTable, out TableOperation operation) 328 private List<Row> CompareTables(WindowsInstallerData targetOutput, Table targetTable, Table updatedTable, out TableOperation operation)
344 { 329 {
345 List<Row> rows = new List<Row>(); 330 var rows = new List<Row>();
346 operation = TableOperation.None; 331 operation = TableOperation.None;
347 332
348 // dropped tables 333 // dropped tables
@@ -360,8 +345,8 @@ namespace WixToolset.Core.WindowsInstaller
360 } 345 }
361 else // possibly modified tables 346 else // possibly modified tables
362 { 347 {
363 SortedList updatedPrimaryKeys = new SortedList(); 348 var updatedPrimaryKeys = new SortedDictionary<string, Row>();
364 SortedList targetPrimaryKeys = new SortedList(); 349 var targetPrimaryKeys = new SortedDictionary<string, Row>();
365 350
366 // compare the table definitions 351 // compare the table definitions
367 if (0 != targetTable.Definition.CompareTo(updatedTable.Definition)) 352 if (0 != targetTable.Definition.CompareTo(updatedTable.Definition))
@@ -374,13 +359,10 @@ namespace WixToolset.Core.WindowsInstaller
374 this.IndexPrimaryKeys(targetTable, targetPrimaryKeys, updatedTable, updatedPrimaryKeys); 359 this.IndexPrimaryKeys(targetTable, targetPrimaryKeys, updatedTable, updatedPrimaryKeys);
375 360
376 // diff the target and updated rows 361 // diff the target and updated rows
377 foreach (DictionaryEntry targetPrimaryKeyEntry in targetPrimaryKeys) 362 foreach (var targetPrimaryKeyEntry in targetPrimaryKeys)
378 { 363 {
379 string targetPrimaryKey = (string)targetPrimaryKeyEntry.Key; 364 var targetPrimaryKey = targetPrimaryKeyEntry.Key;
380 bool keepRow = false; 365 var compared = this.CompareRows(targetTable, targetPrimaryKeyEntry.Value, updatedPrimaryKeys[targetPrimaryKey], out var _, out var keepRow);
381 RowOperation rowOperation = RowOperation.None;
382
383 Row compared = this.CompareRows(targetTable, targetPrimaryKeyEntry.Value as Row, updatedPrimaryKeys[targetPrimaryKey] as Row, out rowOperation, out keepRow);
384 366
385 if (keepRow) 367 if (keepRow)
386 { 368 {
@@ -389,13 +371,13 @@ namespace WixToolset.Core.WindowsInstaller
389 } 371 }
390 372
391 // find the inserted rows 373 // find the inserted rows
392 foreach (DictionaryEntry updatedPrimaryKeyEntry in updatedPrimaryKeys) 374 foreach (var updatedPrimaryKeyEntry in updatedPrimaryKeys)
393 { 375 {
394 string updatedPrimaryKey = (string)updatedPrimaryKeyEntry.Key; 376 var updatedPrimaryKey = (string)updatedPrimaryKeyEntry.Key;
395 377
396 if (!targetPrimaryKeys.Contains(updatedPrimaryKey)) 378 if (!targetPrimaryKeys.ContainsKey(updatedPrimaryKey))
397 { 379 {
398 Row updatedRow = (Row)updatedPrimaryKeyEntry.Value; 380 var updatedRow = (Row)updatedPrimaryKeyEntry.Value;
399 381
400 updatedRow.Operation = RowOperation.Add; 382 updatedRow.Operation = RowOperation.Add;
401 updatedRow.SectionId = sectionDelimiter + updatedRow.SectionId; 383 updatedRow.SectionId = sectionDelimiter + updatedRow.SectionId;
@@ -408,10 +390,10 @@ namespace WixToolset.Core.WindowsInstaller
408 return rows; 390 return rows;
409 } 391 }
410 392
411 private void IndexPrimaryKeys(Table targetTable, SortedList targetPrimaryKeys, Table updatedTable, SortedList updatedPrimaryKeys) 393 private void IndexPrimaryKeys(Table targetTable, SortedDictionary<string, Row> targetPrimaryKeys, Table updatedTable, SortedDictionary<string, Row> updatedPrimaryKeys)
412 { 394 {
413 // index the target rows 395 // index the target rows
414 foreach (Row row in targetTable.Rows) 396 foreach (var row in targetTable.Rows)
415 { 397 {
416 this.AddIndexedRow(targetPrimaryKeys, row); 398 this.AddIndexedRow(targetPrimaryKeys, row);
417 399
@@ -452,7 +434,7 @@ namespace WixToolset.Core.WindowsInstaller
452 } 434 }
453 435
454 // index the updated rows 436 // index the updated rows
455 foreach (Row row in updatedTable.Rows) 437 foreach (var row in updatedTable.Rows)
456 { 438 {
457 this.AddIndexedRow(updatedPrimaryKeys, row); 439 this.AddIndexedRow(updatedPrimaryKeys, row);
458 440
@@ -492,17 +474,15 @@ namespace WixToolset.Core.WindowsInstaller
492 private void UpdateTransformSummaryInformationTable(Table summaryInfoTable, TransformFlags validationFlags) 474 private void UpdateTransformSummaryInformationTable(Table summaryInfoTable, TransformFlags validationFlags)
493 { 475 {
494 // calculate the minimum version of MSI required to process the transform 476 // calculate the minimum version of MSI required to process the transform
495 int targetMin; 477 var minimumVersion = 100;
496 int updatedMin;
497 int minimumVersion = 100;
498 478
499 if (Int32.TryParse(this.transformSummaryInfo.TargetMinimumVersion, out targetMin) && Int32.TryParse(this.transformSummaryInfo.UpdatedMinimumVersion, out updatedMin)) 479 if (Int32.TryParse(this.transformSummaryInfo.TargetMinimumVersion, out var targetMin) && Int32.TryParse(this.transformSummaryInfo.UpdatedMinimumVersion, out var updatedMin))
500 { 480 {
501 minimumVersion = Math.Max(targetMin, updatedMin); 481 minimumVersion = Math.Max(targetMin, updatedMin);
502 } 482 }
503 483
504 Hashtable summaryRows = new Hashtable(summaryInfoTable.Rows.Count); 484 var summaryRows = new Hashtable(summaryInfoTable.Rows.Count);
505 foreach (Row row in summaryInfoTable.Rows) 485 foreach (var row in summaryInfoTable.Rows)
506 { 486 {
507 summaryRows[row[0]] = row; 487 summaryRows[row[0]] = row;
508 488
@@ -535,35 +515,35 @@ namespace WixToolset.Core.WindowsInstaller
535 515
536 if (!summaryRows.Contains((int)SummaryInformation.Transform.TargetPlatformAndLanguage)) 516 if (!summaryRows.Contains((int)SummaryInformation.Transform.TargetPlatformAndLanguage))
537 { 517 {
538 Row summaryRow = summaryInfoTable.CreateRow(null); 518 var summaryRow = summaryInfoTable.CreateRow(null);
539 summaryRow[0] = (int)SummaryInformation.Transform.TargetPlatformAndLanguage; 519 summaryRow[0] = (int)SummaryInformation.Transform.TargetPlatformAndLanguage;
540 summaryRow[1] = this.transformSummaryInfo.TargetPlatformAndLanguage; 520 summaryRow[1] = this.transformSummaryInfo.TargetPlatformAndLanguage;
541 } 521 }
542 522
543 if (!summaryRows.Contains((int)SummaryInformation.Transform.UpdatedPlatformAndLanguage)) 523 if (!summaryRows.Contains((int)SummaryInformation.Transform.UpdatedPlatformAndLanguage))
544 { 524 {
545 Row summaryRow = summaryInfoTable.CreateRow(null); 525 var summaryRow = summaryInfoTable.CreateRow(null);
546 summaryRow[0] = (int)SummaryInformation.Transform.UpdatedPlatformAndLanguage; 526 summaryRow[0] = (int)SummaryInformation.Transform.UpdatedPlatformAndLanguage;
547 summaryRow[1] = this.transformSummaryInfo.UpdatedPlatformAndLanguage; 527 summaryRow[1] = this.transformSummaryInfo.UpdatedPlatformAndLanguage;
548 } 528 }
549 529
550 if (!summaryRows.Contains((int)SummaryInformation.Transform.ValidationFlags)) 530 if (!summaryRows.Contains((int)SummaryInformation.Transform.ValidationFlags))
551 { 531 {
552 Row summaryRow = summaryInfoTable.CreateRow(null); 532 var summaryRow = summaryInfoTable.CreateRow(null);
553 summaryRow[0] = (int)SummaryInformation.Transform.ValidationFlags; 533 summaryRow[0] = (int)SummaryInformation.Transform.ValidationFlags;
554 summaryRow[1] = ((int)validationFlags).ToString(CultureInfo.InvariantCulture); 534 summaryRow[1] = ((int)validationFlags).ToString(CultureInfo.InvariantCulture);
555 } 535 }
556 536
557 if (!summaryRows.Contains((int)SummaryInformation.Transform.InstallerRequirement)) 537 if (!summaryRows.Contains((int)SummaryInformation.Transform.InstallerRequirement))
558 { 538 {
559 Row summaryRow = summaryInfoTable.CreateRow(null); 539 var summaryRow = summaryInfoTable.CreateRow(null);
560 summaryRow[0] = (int)SummaryInformation.Transform.InstallerRequirement; 540 summaryRow[0] = (int)SummaryInformation.Transform.InstallerRequirement;
561 summaryRow[1] = minimumVersion.ToString(CultureInfo.InvariantCulture); 541 summaryRow[1] = minimumVersion.ToString(CultureInfo.InvariantCulture);
562 } 542 }
563 543
564 if (!summaryRows.Contains((int)SummaryInformation.Transform.Security)) 544 if (!summaryRows.Contains((int)SummaryInformation.Transform.Security))
565 { 545 {
566 Row summaryRow = summaryInfoTable.CreateRow(null); 546 var summaryRow = summaryInfoTable.CreateRow(null);
567 summaryRow[0] = (int)SummaryInformation.Transform.Security; 547 summaryRow[0] = (int)SummaryInformation.Transform.Security;
568 summaryRow[1] = "4"; 548 summaryRow[1] = "4";
569 } 549 }
@@ -606,5 +586,3 @@ namespace WixToolset.Core.WindowsInstaller
606 } 586 }
607 } 587 }
608} 588}
609
610#endif
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/MsiBackend.cs b/src/wix/WixToolset.Core.WindowsInstaller/MsiBackend.cs
index 33aa7a74..628ad8de 100644
--- a/src/wix/WixToolset.Core.WindowsInstaller/MsiBackend.cs
+++ b/src/wix/WixToolset.Core.WindowsInstaller/MsiBackend.cs
@@ -2,6 +2,7 @@
2 2
3namespace WixToolset.Core.WindowsInstaller 3namespace WixToolset.Core.WindowsInstaller
4{ 4{
5 using System;
5 using WixToolset.Core.WindowsInstaller.Bind; 6 using WixToolset.Core.WindowsInstaller.Bind;
6 using WixToolset.Core.WindowsInstaller.Decompile; 7 using WixToolset.Core.WindowsInstaller.Decompile;
7 using WixToolset.Core.WindowsInstaller.Unbind; 8 using WixToolset.Core.WindowsInstaller.Unbind;
@@ -71,8 +72,7 @@ namespace WixToolset.Core.WindowsInstaller
71 72
72 public Intermediate Unbind(IUnbindContext context) 73 public Intermediate Unbind(IUnbindContext context)
73 { 74 {
74 var command = new UnbindMsiOrMsmCommand(context); 75 throw new NotImplementedException();
75 return command.Execute();
76 } 76 }
77 } 77 }
78} 78}
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/MsmBackend.cs b/src/wix/WixToolset.Core.WindowsInstaller/MsmBackend.cs
index 02ea5f45..01e3c6d8 100644
--- a/src/wix/WixToolset.Core.WindowsInstaller/MsmBackend.cs
+++ b/src/wix/WixToolset.Core.WindowsInstaller/MsmBackend.cs
@@ -2,6 +2,7 @@
2 2
3namespace WixToolset.Core.WindowsInstaller 3namespace WixToolset.Core.WindowsInstaller
4{ 4{
5 using System;
5 using WixToolset.Core.WindowsInstaller.Bind; 6 using WixToolset.Core.WindowsInstaller.Bind;
6 using WixToolset.Core.WindowsInstaller.Decompile; 7 using WixToolset.Core.WindowsInstaller.Decompile;
7 using WixToolset.Core.WindowsInstaller.Unbind; 8 using WixToolset.Core.WindowsInstaller.Unbind;
@@ -67,8 +68,7 @@ namespace WixToolset.Core.WindowsInstaller
67 68
68 public Intermediate Unbind(IUnbindContext context) 69 public Intermediate Unbind(IUnbindContext context)
69 { 70 {
70 var command = new UnbindMsiOrMsmCommand(context); 71 throw new NotImplementedException();
71 return command.Execute();
72 } 72 }
73 } 73 }
74} 74}
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/MspBackend.cs b/src/wix/WixToolset.Core.WindowsInstaller/MspBackend.cs
index d1f5eb99..398fc780 100644
--- a/src/wix/WixToolset.Core.WindowsInstaller/MspBackend.cs
+++ b/src/wix/WixToolset.Core.WindowsInstaller/MspBackend.cs
@@ -4,13 +4,7 @@ namespace WixToolset.Core.WindowsInstaller
4{ 4{
5 using System; 5 using System;
6 using System.Collections.Generic; 6 using System.Collections.Generic;
7 using System.IO;
8 using System.Linq;
9 using WixToolset.Core.WindowsInstaller.Bind; 7 using WixToolset.Core.WindowsInstaller.Bind;
10 using WixToolset.Core.Native.Msi;
11 using WixToolset.Core.WindowsInstaller.Unbind;
12 using WixToolset.Data;
13 using WixToolset.Data.Symbols;
14 using WixToolset.Data.WindowsInstaller; 8 using WixToolset.Data.WindowsInstaller;
15 using WixToolset.Extensibility; 9 using WixToolset.Extensibility;
16 using WixToolset.Extensibility.Data; 10 using WixToolset.Extensibility.Data;
@@ -74,9 +68,9 @@ namespace WixToolset.Core.WindowsInstaller
74 throw new NotImplementedException(); 68 throw new NotImplementedException();
75 } 69 }
76 70
71#if TODO_PATCHING
77 public Intermediate Unbind(IUnbindContext context) 72 public Intermediate Unbind(IUnbindContext context)
78 { 73 {
79#if TODO_PATCHING
80 Output patch; 74 Output patch;
81 75
82 // patch files are essentially database files (use a special flag to let the API know its a patch file) 76 // patch files are essentially database files (use a special flag to let the API know its a patch file)
@@ -156,8 +150,7 @@ namespace WixToolset.Core.WindowsInstaller
156 } 150 }
157 151
158 return patch; 152 return patch;
159#endif
160 throw new NotImplementedException();
161 } 153 }
154#endif
162 } 155 }
163} 156}
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/MstBackend.cs b/src/wix/WixToolset.Core.WindowsInstaller/MstBackend.cs
deleted file mode 100644
index 8ce75265..00000000
--- a/src/wix/WixToolset.Core.WindowsInstaller/MstBackend.cs
+++ /dev/null
@@ -1,39 +0,0 @@
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
3namespace WixToolset.Core.WindowsInstaller
4{
5 using System;
6 using WixToolset.Core.WindowsInstaller.Unbind;
7 using WixToolset.Data;
8 using WixToolset.Extensibility;
9 using WixToolset.Extensibility.Data;
10
11 internal class MstBackend : IBackend
12 {
13 public IBindResult Bind(IBindContext context)
14 {
15#if TODO_PATCHING
16 var command = new BindTransformCommand();
17 command.Extensions = context.Extensions;
18 command.TempFilesLocation = context.IntermediateFolder;
19 command.Transform = context.IntermediateRepresentation;
20 command.OutputPath = context.OutputPath;
21 command.Execute();
22
23 return new BindResult(Array.Empty<FileTransfer>(), Array.Empty<string>());
24#endif
25 throw new NotImplementedException();
26 }
27
28 public IDecompileResult Decompile(IDecompileContext context)
29 {
30 throw new NotImplementedException();
31 }
32
33 public Intermediate Unbind(IUnbindContext context)
34 {
35 var command = new UnbindMsiOrMsmCommand(context);
36 return command.Execute();
37 }
38 }
39}
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindMsiOrMsmCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindMsiOrMsmCommand.cs
index 75ee6307..82015cf2 100644
--- a/src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindMsiOrMsmCommand.cs
+++ b/src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindMsiOrMsmCommand.cs
@@ -4,52 +4,80 @@ namespace WixToolset.Core.WindowsInstaller.Unbind
4{ 4{
5 using System; 5 using System;
6 using System.ComponentModel; 6 using System.ComponentModel;
7 using WixToolset.Core.Native.Msi;
7 using WixToolset.Data; 8 using WixToolset.Data;
9 using WixToolset.Data.WindowsInstaller;
8 using WixToolset.Extensibility.Data; 10 using WixToolset.Extensibility.Data;
9 using WixToolset.Core.Native.Msi; 11 using WixToolset.Extensibility.Services;
10 12
11 internal class UnbindMsiOrMsmCommand 13 internal class UnbindMsiOrMsmCommand
12 { 14 {
15 public UnbindMsiOrMsmCommand(IMessaging messaging, IBackendHelper backendHelper, string databasePath, string exportBasePath, string intermediateFolder, bool adminImage, bool suppressDemodularization, bool suppressExtractCabinets)
16 {
17 this.Messaging = messaging;
18 this.BackendHelper = backendHelper;
19 this.DatabasePath = databasePath;
20 this.ExportBasePath = exportBasePath;
21 this.IntermediateFolder = intermediateFolder;
22 this.IsAdminImage = adminImage;
23 this.SuppressDemodularization = suppressDemodularization;
24 this.SuppressExtractCabinets = suppressExtractCabinets;
25 }
26
13 public UnbindMsiOrMsmCommand(IUnbindContext context) 27 public UnbindMsiOrMsmCommand(IUnbindContext context)
14 { 28 {
15 this.Context = context; 29 this.Messaging = context.ServiceProvider.GetService<IMessaging>();
30 this.DatabasePath = context.InputFilePath;
31 this.ExportBasePath = context.ExportBasePath;
32 this.IntermediateFolder = context.IntermediateFolder;
33 this.IsAdminImage = context.IsAdminImage;
34 this.SuppressDemodularization = context.SuppressDemodularization;
16 } 35 }
17 36
18 public IUnbindContext Context { get; } 37 private IMessaging Messaging { get; }
19 38
20 public Intermediate Execute() 39 private IBackendHelper BackendHelper { get; }
21 { 40
22#if TODO_PATCHING 41 private string DatabasePath { get; }
23 Output output; 42
43 private string ExportBasePath { get; }
24 44
45 private string IntermediateFolder { get; }
46
47 private bool IsAdminImage { get; }
48
49 private bool SuppressDemodularization { get; }
50
51 private bool SuppressExtractCabinets { get; }
52
53 public WindowsInstallerData Execute()
54 {
25 try 55 try
26 { 56 {
27 using (Database database = new Database(this.Context.InputFilePath, OpenDatabase.ReadOnly)) 57 using (var database = new Database(this.DatabasePath, OpenDatabase.ReadOnly))
28 { 58 {
29 var unbindCommand = new UnbindDatabaseCommand(this.Context.Messaging, database, this.Context.InputFilePath, OutputType.Product, this.Context.ExportBasePath, this.Context.IntermediateFolder, this.Context.IsAdminImage, this.Context.SuppressDemodularization, skipSummaryInfo: false); 59 var unbindCommand = new UnbindDatabaseCommand(this.Messaging, this.BackendHelper, database, this.DatabasePath, OutputType.Product, this.ExportBasePath, this.IntermediateFolder, this.IsAdminImage, this.SuppressDemodularization, skipSummaryInfo: false);
30 output = unbindCommand.Execute(); 60 var data = unbindCommand.Execute();
31 61
32 // extract the files from the cabinets 62 // extract the files from the cabinets
33 if (!String.IsNullOrEmpty(this.Context.ExportBasePath) && !this.Context.SuppressExtractCabinets) 63 if (!String.IsNullOrEmpty(this.ExportBasePath) && !this.SuppressExtractCabinets)
34 { 64 {
35 var extractCommand = new ExtractCabinetsCommand(output, database, this.Context.InputFilePath, this.Context.ExportBasePath, this.Context.IntermediateFolder); 65 var extractCommand = new ExtractCabinetsCommand(data, database, this.DatabasePath, this.ExportBasePath, this.IntermediateFolder);
36 extractCommand.Execute(); 66 extractCommand.Execute();
37 } 67 }
68
69 return data;
38 } 70 }
39 } 71 }
40 catch (Win32Exception e) 72 catch (Win32Exception e)
41 { 73 {
42 if (0x6E == e.NativeErrorCode) // ERROR_OPEN_FAILED 74 if (0x6E == e.NativeErrorCode) // ERROR_OPEN_FAILED
43 { 75 {
44 throw new WixException(WixErrors.OpenDatabaseFailed(this.Context.InputFilePath)); 76 //throw new WixException(WixErrors.OpenDatabaseFailed(this.DatabasePath));
45 } 77 }
46 78
47 throw; 79 throw;
48 } 80 }
49
50 return output;
51#endif
52 throw new NotImplementedException();
53 } 81 }
54 } 82 }
55} 83}
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindTranformCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindTransformCommand.cs
index f40aed4e..ea40fa9f 100644
--- a/src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindTranformCommand.cs
+++ b/src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindTransformCommand.cs
@@ -41,7 +41,7 @@ namespace WixToolset.Core.WindowsInstaller.Unbind
41 private TableDefinitionCollection TableDefinitions { get; } 41 private TableDefinitionCollection TableDefinitions { get; }
42 42
43 private string EmptyFile { get; set; } 43 private string EmptyFile { get; set; }
44 44
45 public WindowsInstallerData Execute() 45 public WindowsInstallerData Execute()
46 { 46 {
47 var transform = new WindowsInstallerData(new SourceLineNumber(this.TransformFile)); 47 var transform = new WindowsInstallerData(new SourceLineNumber(this.TransformFile));
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/WindowsInstallerBackendErrors.cs b/src/wix/WixToolset.Core.WindowsInstaller/WindowsInstallerBackendErrors.cs
index 0c15ad05..2efb06f1 100644
--- a/src/wix/WixToolset.Core.WindowsInstaller/WindowsInstallerBackendErrors.cs
+++ b/src/wix/WixToolset.Core.WindowsInstaller/WindowsInstallerBackendErrors.cs
@@ -2,14 +2,17 @@
2 2
3namespace WixToolset.Core.WindowsInstaller 3namespace WixToolset.Core.WindowsInstaller
4{ 4{
5 using System;
5 using WixToolset.Data; 6 using WixToolset.Data;
6 7
7 internal static class WindowsInstallerBackendErrors 8 internal static class WindowsInstallerBackendErrors
8 { 9 {
9 //public static Message ReplaceThisWithTheFirstError(SourceLineNumber sourceLineNumbers) 10 public static Message CannotLoadWixoutAsTransform(SourceLineNumber sourceLineNumbers, Exception exception)
10 //{ 11 {
11 // return Message(sourceLineNumbers, Ids.ReplaceThisWithTheFirstError, "format string", arg1, arg2); 12 var additionalDetail = exception == null ? String.Empty : ", detail: " + exception.Message;
12 //} 13
14 return Message(sourceLineNumbers, Ids.CannotLoadWixoutAsTransform, "Could not load wixout file as a transform{1}", additionalDetail);
15 }
13 16
14 private static Message Message(SourceLineNumber sourceLineNumber, Ids id, string format, params object[] args) 17 private static Message Message(SourceLineNumber sourceLineNumber, Ids id, string format, params object[] args)
15 { 18 {
@@ -18,7 +21,7 @@ namespace WixToolset.Core.WindowsInstaller
18 21
19 public enum Ids 22 public enum Ids
20 { 23 {
21 // ReplaceThisWithTheFirstError = 7500, 24 CannotLoadWixoutAsTransform = 7500,
22 } // last available is 7999. 8000 is BurnBackendErrors. 25 } // last available is 7999. 8000 is BurnBackendErrors.
23 } 26 }
24} 27}
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/WindowsInstallerBackendFactory.cs b/src/wix/WixToolset.Core.WindowsInstaller/WindowsInstallerBackendFactory.cs
index f72acb21..d14743e9 100644
--- a/src/wix/WixToolset.Core.WindowsInstaller/WindowsInstallerBackendFactory.cs
+++ b/src/wix/WixToolset.Core.WindowsInstaller/WindowsInstallerBackendFactory.cs
@@ -37,11 +37,6 @@ namespace WixToolset.Core.WindowsInstaller
37 //case "patchcreation": 37 //case "patchcreation":
38 //case ".pcp": 38 //case ".pcp":
39 // return new PatchCreationBackend(); 39 // return new PatchCreationBackend();
40
41 case "transform":
42 case ".mst":
43 backend = new MstBackend();
44 return true;
45 } 40 }
46 41
47 backend = null; 42 backend = null;
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/Language/Package.en-us.wxl b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/Language/Package.en-us.wxl
index f7453566..d3844e39 100644
--- a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/Language/Package.en-us.wxl
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/Language/Package.en-us.wxl
@@ -1,7 +1,7 @@
1<?xml version="1.0" encoding="utf-8"?> 1<?xml version="1.0" encoding="utf-8"?>
2<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="en-US"> 2<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="en-US">
3 3
4 <String Id="DowngradeError">A newer version of [ProductName] is already installed.</String> 4 <String Id="DowngradeError">A newer version en-us of [ProductName] is already installed.</String>
5 <String Id="FeatureTitle">MsiPackage</String> 5 <String Id="FeatureTitle">MsiPackage en-us</String>
6 6
7</WixLocalization> 7</WixLocalization>
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/Language/Package.ja-jp.wxl b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/Language/Package.ja-jp.wxl
index ef287da7..48a339d3 100644
--- a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/Language/Package.ja-jp.wxl
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/Language/Package.ja-jp.wxl
@@ -1,7 +1,7 @@
1<?xml version="1.0" encoding="utf-8"?> 1<?xml version="1.0" encoding="utf-8"?>
2<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="ja-JP"> 2<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="ja-JP">
3 3
4 <String Id="DowngradeError">A newer version of [ProductName] is already installed.</String> 4 <String Id="DowngradeError">A newer version ja-jp of [ProductName] is already installed.</String>
5 <String Id="FeatureTitle">MsiPackage</String> 5 <String Id="FeatureTitle">MsiPackage ja-jp</String>
6 6
7</WixLocalization> 7</WixLocalization>
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TransformFixture.cs b/src/wix/test/WixToolsetTest.CoreIntegration/TransformFixture.cs
new file mode 100644
index 00000000..bdbf5c26
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TransformFixture.cs
@@ -0,0 +1,143 @@
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
3namespace WixToolsetTest.CoreIntegration
4{
5 using System.IO;
6 using System.Linq;
7 using WixBuildTools.TestSupport;
8 using WixToolset.Core.TestPackage;
9 using WixToolset.Data.WindowsInstaller;
10 using Xunit;
11
12 public class TransformFixture
13 {
14 [Fact]
15 public void CanBuildTransformFromEnuToJpn()
16 {
17 var folder = TestData.Get(@"TestData", "Language");
18
19 using (var fs = new DisposableFileSystem())
20 {
21 var baseFolder = fs.GetFolder();
22 var enuMsiPath = Path.Combine(baseFolder, @"bin\enu.msi");
23 var jpnMsiPath = Path.Combine(baseFolder, @"bin\jpn.msi");
24 var mstPath = Path.Combine(baseFolder, @"bin\test.mst");
25
26 var result = WixRunner.Execute(new[]
27 {
28 "build",
29 Path.Combine(folder, "Package.wxs"),
30 "-loc", Path.Combine(folder, "Package.en-us.wxl"),
31 "-bindpath", Path.Combine(folder, "data"),
32 "-intermediateFolder", Path.Combine(baseFolder, "obj"),
33 "-o", enuMsiPath
34 });
35 result.AssertSuccess();
36
37
38 result = WixRunner.Execute(new[]
39 {
40 "build",
41 Path.Combine(folder, "Package.wxs"),
42 "-loc", Path.Combine(folder, "Package.ja-jp.wxl"),
43 "-bindpath", Path.Combine(folder, "data"),
44 "-intermediateFolder", Path.Combine(baseFolder, "obj"),
45 "-o", jpnMsiPath
46 });
47 result.AssertSuccess();
48
49 result = WixRunner.Execute(new[]
50 {
51 "msi", "transform",
52 "-intermediateFolder", Path.Combine(baseFolder, "obj"),
53 "-serr", "f",
54 "-o", mstPath,
55 enuMsiPath,
56 jpnMsiPath
57 });
58 result.AssertSuccess();
59
60 Assert.True(File.Exists(mstPath));
61 }
62 }
63
64 [Fact]
65 public void CanBuildWixoutTransform()
66 {
67 var folder = TestData.Get(@"TestData", "Language");
68
69 using (var fs = new DisposableFileSystem())
70 {
71 var baseFolder = fs.GetFolder();
72 var enuMsiPath = Path.Combine(baseFolder, @"bin\enu.msi");
73 var jpnMsiPath = Path.Combine(baseFolder, @"bin\jpn.msi");
74 var wixmstPath = Path.Combine(baseFolder, @"bin\test.wixmst");
75 var mstPath = Path.Combine(baseFolder, @"bin\test.mst");
76
77 var result = WixRunner.Execute(new[]
78 {
79 "build",
80 Path.Combine(folder, "Package.wxs"),
81 "-loc", Path.Combine(folder, "Package.en-us.wxl"),
82 "-bindpath", Path.Combine(folder, "data"),
83 "-intermediateFolder", Path.Combine(baseFolder, "obj"),
84 "-o", enuMsiPath
85 });
86 result.AssertSuccess();
87
88 result = WixRunner.Execute(new[]
89 {
90 "build",
91 Path.Combine(folder, "Package.wxs"),
92 "-loc", Path.Combine(folder, "Package.ja-jp.wxl"),
93 "-bindpath", Path.Combine(folder, "data"),
94 "-intermediateFolder", Path.Combine(baseFolder, "obj"),
95 "-o", jpnMsiPath
96 });
97 result.AssertSuccess();
98
99 result = WixRunner.Execute(new[]
100 {
101 "msi", "transform",
102 "-intermediateFolder", Path.Combine(baseFolder, "obj"),
103 "-serr", "f",
104 "-xo",
105 "-o", wixmstPath,
106 enuMsiPath,
107 jpnMsiPath
108 });
109 result.AssertSuccess();
110
111 var wixmst = WindowsInstallerData.Load(wixmstPath);
112 var rows = wixmst.Tables.SelectMany(t => t.Rows).Where(r => r.Operation == RowOperation.Modify).ToDictionary(r => r.GetPrimaryKey());
113
114 WixAssert.CompareLineByLine(new[]
115 {
116 "NOT WIX_DOWNGRADE_DETECTED",
117 "ProductCode",
118 "ProductFeature",
119 "ProductLanguage"
120 }, rows.Keys.OrderBy(s => s).ToArray());
121
122 Assert.True(rows.TryGetValue("ProductFeature", out var productFeatureRow));
123 Assert.Equal("MsiPackage ja-jp", productFeatureRow.FieldAsString(2));
124
125 Assert.True(rows.TryGetValue("ProductLanguage", out var productLanguageRow));
126 Assert.Equal("1041", productLanguageRow.FieldAsString(1));
127
128 Assert.False(File.Exists(mstPath));
129
130 result = WixRunner.Execute(new[]
131 {
132 "msi", "transform",
133 "-intermediateFolder", Path.Combine(baseFolder, "obj"),
134 "-o", mstPath,
135 wixmstPath
136 });
137 result.AssertSuccess();
138
139 Assert.True(File.Exists(mstPath));
140 }
141 }
142 }
143}