@joebigelow / wix / commits / 91cd2d65

Implement "wix msi transform"

Brings the functionality of torch into the WindowsInstallerBackend as the "transform" subcommand. Fixes 4602

Rob Mensching committed Mar 5, 2022 at 11:09 UTC 91cd2d65121a163e625d2f029025123b0f8467d2
16 files changed +653 -201
src/wix/WixToolset.Core.WindowsInstaller/Bind/BindTransformCommand.cs
+9 -13
@@ -62,18 +62,19 @@ namespace WixToolset.Core.WindowsInstaller.Bind
62
63 if (this.Transform.TryGetTable("Property", out var propertyTable))
64 {
65 - for (int i = propertyTable.Rows.Count - 1; i >= 0; i--)
65 + for (var i = propertyTable.Rows.Count - 1; i >= 0; i--)
66 {
67 - Row row = propertyTable.Rows[i];
67 + var row = propertyTable.Rows[i];
68 + var id = row.FieldAsString(0);
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)
71 {
72 propertyTable.Rows.RemoveAt(i);
72 -
73 - if ("UpgradeCode" == (string)row[0])
74 - {
75 - updatedUpgradeCode = (string)row[1];
76 - }
73 + }
74 + else if ("UpgradeCode" == id)
75 + {
76 + updatedUpgradeCode = id;
77 + propertyTable.Rows.RemoveAt(i);
78 }
79 }
80 }
@@ -383,11 +384,6 @@ namespace WixToolset.Core.WindowsInstaller.Bind
384 }
385 }
386
386 - //foreach (BinderExtension extension in this.Extensions)
387 - //{
388 - // extension.PostBind(this.Context);
389 - //}
390 -
387 // Any errors encountered up to this point can cause errors during generation.
388 if (this.Messaging.EncounteredError)
389 {
src/wix/WixToolset.Core.WindowsInstaller/Bind/LoadTableDefinitionsCommand.cs
+2 -4
@@ -2,9 +2,7 @@
2
3 namespace WixToolset.Core.WindowsInstaller.Bind
4 {
5 - using System;
5 using System.Collections.Generic;
7 - using System.Globalization;
6 using System.Linq;
7 using WixToolset.Data;
8 using WixToolset.Data.Symbols;
@@ -32,9 +30,9 @@ namespace WixToolset.Core.WindowsInstaller.Bind
30 public TableDefinitionCollection Execute()
31 {
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);
34
37 - if (customColumnsById.Any())
35 + if (customColumnsById?.Any() == true)
36 {
37 foreach (var symbol in this.Section.Symbols.OfType<WixCustomTableSymbol>())
38 {
src/wix/WixToolset.Core.WindowsInstaller/CommandLine/TransformSubcommand.cs new
+352
@@ -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 +
3 +namespace 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 +}
src/wix/WixToolset.Core.WindowsInstaller/CommandLine/WindowsInstallerCommand.cs
+5
@@ -49,6 +49,10 @@ namespace WixToolset.Core.WindowsInstaller.CommandLine
49 this.Subcommand = new InscribeSubcommand(this.ServiceProvider);
50 return true;
51
52 + case "transform":
53 + this.Subcommand = new TransformSubcommand(this.ServiceProvider);
54 + return true;
55 +
56 case "validate":
57 this.Subcommand = new ValidateSubcommand(this.ServiceProvider);
58 return true;
@@ -72,6 +76,7 @@ namespace WixToolset.Core.WindowsInstaller.CommandLine
76 Console.WriteLine("Commands:");
77 Console.WriteLine();
78 Console.WriteLine(" inscribe Updates MSI database with cabinet signature information.");
79 + Console.WriteLine(" transform Creates an MST transform file.");
80 Console.WriteLine(" validate Validates MSI database using standard or custom ICEs.");
81 }
82 }
src/wix/WixToolset.Core.WindowsInstaller/Differ.cs
+78 -100
@@ -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.
2
3 -#if DELETE
4 -
3 namespace WixToolset.Core.WindowsInstaller
4 {
5 using System;
6 using System.Collections;
7 using System.Collections.Generic;
8 using System.Globalization;
11 - using WixToolset.Core.WindowsInstaller.Msi;
9 + using WixToolset.Core.Native.Msi;
10 using WixToolset.Data;
11 using WixToolset.Data.Symbols;
12 using WixToolset.Data.WindowsInstaller;
15 - using WixToolset.Data.WindowsInstaller.Rows;
13 using WixToolset.Extensibility;
14 using WixToolset.Extensibility.Services;
15
@@ -22,9 +19,6 @@ namespace WixToolset.Core.WindowsInstaller
19 public sealed class Differ
20 {
21 private readonly List<IInspectorExtension> inspectorExtensions;
25 - private bool showPedanticMessages;
26 - private bool suppressKeepingSpecialRows;
27 - private bool preserveUnchangedRows;
22 private const char sectionDelimiter = '/';
23 private readonly IMessaging messaging;
24 private SummaryInformationStreams transformSummaryInfo;
@@ -42,31 +36,19 @@ namespace WixToolset.Core.WindowsInstaller
36 /// Gets or sets the option to show pedantic messages.
37 /// </summary>
38 /// <value>The option to show pedantic messages.</value>
45 - public bool ShowPedanticMessages
46 - {
47 - get { return this.showPedanticMessages; }
48 - set { this.showPedanticMessages = value; }
49 - }
39 + public bool ShowPedanticMessages { get; set; }
40
41 /// <summary>
42 /// Gets or sets the option to suppress keeping special rows.
43 /// </summary>
44 /// <value>The option to suppress keeping special rows.</value>
55 - public bool SuppressKeepingSpecialRows
56 - {
57 - get { return this.suppressKeepingSpecialRows; }
58 - set { this.suppressKeepingSpecialRows = value; }
59 - }
45 + public bool SuppressKeepingSpecialRows { get; set; }
46
47 /// <summary>
48 /// Gets or sets the flag to determine if all rows, even unchanged ones will be persisted in the output.
49 /// </summary>
50 /// <value>The option to keep all rows including unchanged rows.</value>
65 - public bool PreserveUnchangedRows
66 - {
67 - get { return this.preserveUnchangedRows; }
68 - set { this.preserveUnchangedRows = value; }
69 - }
51 + public bool PreserveUnchangedRows { get; set; }
52
53 /// <summary>
54 /// Adds an extension.
@@ -97,7 +79,7 @@ namespace WixToolset.Core.WindowsInstaller
79 /// <returns>The transform.</returns>
80 public WindowsInstallerData Diff(WindowsInstallerData targetOutput, WindowsInstallerData updatedOutput, TransformFlags validationFlags)
81 {
100 - WindowsInstallerData transform = new WindowsInstallerData(null);
82 + var transform = new WindowsInstallerData(null);
83 transform.Type = OutputType.Transform;
84 transform.Codepage = updatedOutput.Codepage;
85 this.transformSummaryInfo = new SummaryInformationStreams();
@@ -119,34 +101,34 @@ namespace WixToolset.Core.WindowsInstaller
101 }
102
103 // compare the contents of the tables
122 - foreach (Table targetTable in targetOutput.Tables)
104 + foreach (var targetTable in targetOutput.Tables)
105 {
124 - Table updatedTable = updatedOutput.Tables[targetTable.Name];
125 - TableOperation operation = TableOperation.None;
106 + var updatedTable = updatedOutput.Tables[targetTable.Name];
107 + var operation = TableOperation.None;
108
127 - List<Row> rows = this.CompareTables(targetOutput, targetTable, updatedTable, out operation);
109 + var rows = this.CompareTables(targetOutput, targetTable, updatedTable, out operation);
110
111 if (TableOperation.Drop == operation)
112 {
131 - Table droppedTable = transform.EnsureTable(targetTable.Definition);
113 + var droppedTable = transform.EnsureTable(targetTable.Definition);
114 droppedTable.Operation = TableOperation.Drop;
115 }
116 else if (TableOperation.None == operation)
117 {
136 - Table modified = transform.EnsureTable(updatedTable.Definition);
118 + var modified = transform.EnsureTable(updatedTable.Definition);
119 rows.ForEach(r => modified.Rows.Add(r));
120 }
121 }
122
123 // added tables
142 - foreach (Table updatedTable in updatedOutput.Tables)
124 + foreach (var updatedTable in updatedOutput.Tables)
125 {
126 if (null == targetOutput.Tables[updatedTable.Name])
127 {
146 - Table addedTable = transform.EnsureTable(updatedTable.Definition);
128 + var addedTable = transform.EnsureTable(updatedTable.Definition);
129 addedTable.Operation = TableOperation.Add;
130
149 - foreach (Row updatedRow in updatedTable.Rows)
131 + foreach (var updatedRow in updatedTable.Rows)
132 {
133 updatedRow.Operation = RowOperation.Add;
134 updatedRow.SectionId = sectionDelimiter + updatedRow.SectionId;
@@ -156,9 +138,9 @@ namespace WixToolset.Core.WindowsInstaller
138 }
139
140 // set summary information properties
159 - if (!this.suppressKeepingSpecialRows)
141 + if (!this.SuppressKeepingSpecialRows)
142 {
161 - Table summaryInfoTable = transform.Tables["_SummaryInformation"];
143 + var summaryInfoTable = transform.Tables["_SummaryInformation"];
144 this.UpdateTransformSummaryInformationTable(summaryInfoTable, validationFlags);
145 }
146
@@ -170,53 +152,56 @@ namespace WixToolset.Core.WindowsInstaller
152 /// </summary>
153 /// <param name="index">The indexed rows.</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)
156 {
175 - string primaryKey = row.GetPrimaryKey('/');
176 - if (null != primaryKey)
157 + var primaryKey = row.GetPrimaryKey('/');
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))
162 {
178 - // Overriding WixActionRows have a primary key defined and take precedence in the index.
179 - if (row is WixActionRow)
163 + // This is provided for compatibility with unreal tables with no primary key
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
175 {
181 - WixActionRow currentRow = (WixActionRow)row;
182 - if (index.Contains(primaryKey))
176 +#if TODO
177 + // Overriding WixActionRows have a primary key defined and take precedence in the index.
178 + if (row is WixActionRow currentActionRow)
179 {
180 // If the current row is not overridable, see if the indexed row is.
185 - if (!currentRow.Overridable)
181 + if (!currentActionRow.Overridable)
182 {
187 - WixActionRow indexedRow = index[primaryKey] as WixActionRow;
188 - if (null != indexedRow && indexedRow.Overridable)
183 + if (existingRow is WixActionRow existingActionRow && existingActionRow.Overridable)
184 {
185 // The indexed key is overridable and should be replaced
186 // (not removed and re-added which results in two Array.Copy
187 // operations for SortedList, or may be re-hashing in other
188 // implementations of IDictionary).
194 - index[primaryKey] = currentRow;
189 + index[primaryKey] = currentActionRow;
190 }
191 }
192
193 // If we got this far, the row does not need to be indexed.
194 return;
195 }
201 - }
196 +#endif
197
203 - // Nothing else should be added more than once.
204 - if (!index.Contains(primaryKey))
205 - {
206 - index.Add(primaryKey, row);
207 - }
208 - else if (this.showPedanticMessages)
209 - {
210 - this.messaging.Write(ErrorMessages.DuplicatePrimaryKey(row.SourceLineNumbers, primaryKey, row.Table.Name));
198 + // Nothing else should be added more than once.
199 + if (this.ShowPedanticMessages)
200 + {
201 + this.messaging.Write(ErrorMessages.DuplicatePrimaryKey(row.SourceLineNumbers, primaryKey, row.Table.Name));
202 + }
203 }
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 - }
205 }
206
207 private Row CompareRows(Table targetTable, Row targetRow, Row updatedRow, out RowOperation operation, out bool keepRow)
@@ -235,7 +220,7 @@ namespace WixToolset.Core.WindowsInstaller
220 else if (null == updatedRow)
221 {
222 operation = targetRow.Operation = RowOperation.Delete;
238 - targetRow.SectionId = targetRow.SectionId + sectionDelimiter;
223 + targetRow.SectionId += sectionDelimiter;
224 comparedRow = targetRow;
225 keepRow = true;
226 }
@@ -243,7 +228,7 @@ namespace WixToolset.Core.WindowsInstaller
228 else // possibly modified
229 {
230 updatedRow.Operation = RowOperation.None;
246 - if (!this.suppressKeepingSpecialRows && "_SummaryInformation" == targetTable.Name)
231 + if (!this.SuppressKeepingSpecialRows && "_SummaryInformation" == targetTable.Name)
232 {
233 // ignore rows that shouldn't be in a transform
234 if (Enum.IsDefined(typeof(SummaryInformation.Transform), (int)updatedRow[0]))
@@ -256,18 +241,18 @@ namespace WixToolset.Core.WindowsInstaller
241 }
242 else
243 {
259 - if (this.preserveUnchangedRows)
244 + if (this.PreserveUnchangedRows)
245 {
246 keepRow = true;
247 }
248
264 - for (int i = 0; i < updatedRow.Fields.Length; i++)
249 + for (var i = 0; i < updatedRow.Fields.Length; i++)
250 {
266 - ColumnDefinition columnDefinition = updatedRow.Fields[i].Column;
251 + var columnDefinition = updatedRow.Fields[i].Column;
252
253 if (!columnDefinition.PrimaryKey)
254 {
270 - bool modified = false;
255 + var modified = false;
256
257 if (i >= targetRow.Fields.Length)
258 {
@@ -290,12 +275,12 @@ namespace WixToolset.Core.WindowsInstaller
275 updatedRow.Fields[i].PreviousData = (string)targetRow.Fields[i].Data;
276
277 // keep rows containing preserved fields so the historical data is available to the binder
293 - keepRow = !this.suppressKeepingSpecialRows;
278 + keepRow = !this.SuppressKeepingSpecialRows;
279 }
280 else if (ColumnType.Object == columnDefinition.Type)
281 {
297 - ObjectField targetObjectField = (ObjectField)targetRow.Fields[i];
298 - ObjectField updatedObjectField = (ObjectField)updatedRow.Fields[i];
282 + var targetObjectField = (ObjectField)targetRow.Fields[i];
283 + var updatedObjectField = (ObjectField)updatedRow.Fields[i];
284
285 updatedObjectField.PreviousEmbeddedFileIndex = targetObjectField.EmbeddedFileIndex;
286 updatedObjectField.PreviousBaseUri = targetObjectField.BaseUri;
@@ -308,7 +293,7 @@ namespace WixToolset.Core.WindowsInstaller
293 updatedObjectField.UnresolvedPreviousData = (string)targetObjectField.UnresolvedData;
294
295 // keep rows containing object fields so the files can be compared in the binder
311 - keepRow = !this.suppressKeepingSpecialRows;
296 + keepRow = !this.SuppressKeepingSpecialRows;
297 }
298 else
299 {
@@ -342,7 +327,7 @@ namespace WixToolset.Core.WindowsInstaller
327
328 private List<Row> CompareTables(WindowsInstallerData targetOutput, Table targetTable, Table updatedTable, out TableOperation operation)
329 {
345 - List<Row> rows = new List<Row>();
330 + var rows = new List<Row>();
331 operation = TableOperation.None;
332
333 // dropped tables
@@ -360,8 +345,8 @@ namespace WixToolset.Core.WindowsInstaller
345 }
346 else // possibly modified tables
347 {
363 - SortedList updatedPrimaryKeys = new SortedList();
364 - SortedList targetPrimaryKeys = new SortedList();
348 + var updatedPrimaryKeys = new SortedDictionary<string, Row>();
349 + var targetPrimaryKeys = new SortedDictionary<string, Row>();
350
351 // compare the table definitions
352 if (0 != targetTable.Definition.CompareTo(updatedTable.Definition))
@@ -374,13 +359,10 @@ namespace WixToolset.Core.WindowsInstaller
359 this.IndexPrimaryKeys(targetTable, targetPrimaryKeys, updatedTable, updatedPrimaryKeys);
360
361 // diff the target and updated rows
377 - foreach (DictionaryEntry targetPrimaryKeyEntry in targetPrimaryKeys)
362 + foreach (var targetPrimaryKeyEntry in targetPrimaryKeys)
363 {
379 - string targetPrimaryKey = (string)targetPrimaryKeyEntry.Key;
380 - bool keepRow = false;
381 - RowOperation rowOperation = RowOperation.None;
382 -
383 - Row compared = this.CompareRows(targetTable, targetPrimaryKeyEntry.Value as Row, updatedPrimaryKeys[targetPrimaryKey] as Row, out rowOperation, out keepRow);
364 + var targetPrimaryKey = targetPrimaryKeyEntry.Key;
365 + var compared = this.CompareRows(targetTable, targetPrimaryKeyEntry.Value, updatedPrimaryKeys[targetPrimaryKey], out var _, out var keepRow);
366
367 if (keepRow)
368 {
@@ -389,13 +371,13 @@ namespace WixToolset.Core.WindowsInstaller
371 }
372
373 // find the inserted rows
392 - foreach (DictionaryEntry updatedPrimaryKeyEntry in updatedPrimaryKeys)
374 + foreach (var updatedPrimaryKeyEntry in updatedPrimaryKeys)
375 {
394 - string updatedPrimaryKey = (string)updatedPrimaryKeyEntry.Key;
376 + var updatedPrimaryKey = (string)updatedPrimaryKeyEntry.Key;
377
396 - if (!targetPrimaryKeys.Contains(updatedPrimaryKey))
378 + if (!targetPrimaryKeys.ContainsKey(updatedPrimaryKey))
379 {
398 - Row updatedRow = (Row)updatedPrimaryKeyEntry.Value;
380 + var updatedRow = (Row)updatedPrimaryKeyEntry.Value;
381
382 updatedRow.Operation = RowOperation.Add;
383 updatedRow.SectionId = sectionDelimiter + updatedRow.SectionId;
@@ -408,10 +390,10 @@ namespace WixToolset.Core.WindowsInstaller
390 return rows;
391 }
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)
394 {
395 // index the target rows
414 - foreach (Row row in targetTable.Rows)
396 + foreach (var row in targetTable.Rows)
397 {
398 this.AddIndexedRow(targetPrimaryKeys, row);
399
@@ -452,7 +434,7 @@ namespace WixToolset.Core.WindowsInstaller
434 }
435
436 // index the updated rows
455 - foreach (Row row in updatedTable.Rows)
437 + foreach (var row in updatedTable.Rows)
438 {
439 this.AddIndexedRow(updatedPrimaryKeys, row);
440
@@ -492,17 +474,15 @@ namespace WixToolset.Core.WindowsInstaller
474 private void UpdateTransformSummaryInformationTable(Table summaryInfoTable, TransformFlags validationFlags)
475 {
476 // calculate the minimum version of MSI required to process the transform
495 - int targetMin;
496 - int updatedMin;
497 - int minimumVersion = 100;
477 + var minimumVersion = 100;
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))
480 {
481 minimumVersion = Math.Max(targetMin, updatedMin);
482 }
483
504 - Hashtable summaryRows = new Hashtable(summaryInfoTable.Rows.Count);
505 - foreach (Row row in summaryInfoTable.Rows)
484 + var summaryRows = new Hashtable(summaryInfoTable.Rows.Count);
485 + foreach (var row in summaryInfoTable.Rows)
486 {
487 summaryRows[row[0]] = row;
488
@@ -535,35 +515,35 @@ namespace WixToolset.Core.WindowsInstaller
515
516 if (!summaryRows.Contains((int)SummaryInformation.Transform.TargetPlatformAndLanguage))
517 {
538 - Row summaryRow = summaryInfoTable.CreateRow(null);
518 + var summaryRow = summaryInfoTable.CreateRow(null);
519 summaryRow[0] = (int)SummaryInformation.Transform.TargetPlatformAndLanguage;
520 summaryRow[1] = this.transformSummaryInfo.TargetPlatformAndLanguage;
521 }
522
523 if (!summaryRows.Contains((int)SummaryInformation.Transform.UpdatedPlatformAndLanguage))
524 {
545 - Row summaryRow = summaryInfoTable.CreateRow(null);
525 + var summaryRow = summaryInfoTable.CreateRow(null);
526 summaryRow[0] = (int)SummaryInformation.Transform.UpdatedPlatformAndLanguage;
527 summaryRow[1] = this.transformSummaryInfo.UpdatedPlatformAndLanguage;
528 }
529
530 if (!summaryRows.Contains((int)SummaryInformation.Transform.ValidationFlags))
531 {
552 - Row summaryRow = summaryInfoTable.CreateRow(null);
532 + var summaryRow = summaryInfoTable.CreateRow(null);
533 summaryRow[0] = (int)SummaryInformation.Transform.ValidationFlags;
534 summaryRow[1] = ((int)validationFlags).ToString(CultureInfo.InvariantCulture);
535 }
536
537 if (!summaryRows.Contains((int)SummaryInformation.Transform.InstallerRequirement))
538 {
559 - Row summaryRow = summaryInfoTable.CreateRow(null);
539 + var summaryRow = summaryInfoTable.CreateRow(null);
540 summaryRow[0] = (int)SummaryInformation.Transform.InstallerRequirement;
541 summaryRow[1] = minimumVersion.ToString(CultureInfo.InvariantCulture);
542 }
543
544 if (!summaryRows.Contains((int)SummaryInformation.Transform.Security))
545 {
566 - Row summaryRow = summaryInfoTable.CreateRow(null);
546 + var summaryRow = summaryInfoTable.CreateRow(null);
547 summaryRow[0] = (int)SummaryInformation.Transform.Security;
548 summaryRow[1] = "4";
549 }
@@ -606,5 +586,3 @@ namespace WixToolset.Core.WindowsInstaller
586 }
587 }
588 }
609 -
610 -#endif
src/wix/WixToolset.Core.WindowsInstaller/MsiBackend.cs
+2 -2
@@ -2,6 +2,7 @@
2
3 namespace WixToolset.Core.WindowsInstaller
4 {
5 + using System;
6 using WixToolset.Core.WindowsInstaller.Bind;
7 using WixToolset.Core.WindowsInstaller.Decompile;
8 using WixToolset.Core.WindowsInstaller.Unbind;
@@ -71,8 +72,7 @@ namespace WixToolset.Core.WindowsInstaller
72
73 public Intermediate Unbind(IUnbindContext context)
74 {
74 - var command = new UnbindMsiOrMsmCommand(context);
75 - return command.Execute();
75 + throw new NotImplementedException();
76 }
77 }
78 }
src/wix/WixToolset.Core.WindowsInstaller/MsmBackend.cs
+2 -2
@@ -2,6 +2,7 @@
2
3 namespace WixToolset.Core.WindowsInstaller
4 {
5 + using System;
6 using WixToolset.Core.WindowsInstaller.Bind;
7 using WixToolset.Core.WindowsInstaller.Decompile;
8 using WixToolset.Core.WindowsInstaller.Unbind;
@@ -67,8 +68,7 @@ namespace WixToolset.Core.WindowsInstaller
68
69 public Intermediate Unbind(IUnbindContext context)
70 {
70 - var command = new UnbindMsiOrMsmCommand(context);
71 - return command.Execute();
71 + throw new NotImplementedException();
72 }
73 }
74 }
src/wix/WixToolset.Core.WindowsInstaller/MspBackend.cs
+2 -9
@@ -4,13 +4,7 @@ namespace WixToolset.Core.WindowsInstaller
4 {
5 using System;
6 using System.Collections.Generic;
7 - using System.IO;
8 - using System.Linq;
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;
8 using WixToolset.Data.WindowsInstaller;
9 using WixToolset.Extensibility;
10 using WixToolset.Extensibility.Data;
@@ -74,9 +68,9 @@ namespace WixToolset.Core.WindowsInstaller
68 throw new NotImplementedException();
69 }
70
71 +#if TODO_PATCHING
72 public Intermediate Unbind(IUnbindContext context)
73 {
79 -#if TODO_PATCHING
74 Output patch;
75
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
150 }
151
152 return patch;
159 -#endif
160 - throw new NotImplementedException();
153 }
154 +#endif
155 }
156 }
src/wix/WixToolset.Core.WindowsInstaller/MstBackend.cs deleted
-39
@@ -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 -
3 -namespace 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 -}
src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindMsiOrMsmCommand.cs
+45 -17
@@ -4,52 +4,80 @@ namespace WixToolset.Core.WindowsInstaller.Unbind
4 {
5 using System;
6 using System.ComponentModel;
7 + using WixToolset.Core.Native.Msi;
8 using WixToolset.Data;
9 + using WixToolset.Data.WindowsInstaller;
10 using WixToolset.Extensibility.Data;
9 - using WixToolset.Core.Native.Msi;
11 + using WixToolset.Extensibility.Services;
12
13 internal class UnbindMsiOrMsmCommand
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 +
27 public UnbindMsiOrMsmCommand(IUnbindContext context)
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;
35 }
36
18 - public IUnbindContext Context { get; }
37 + private IMessaging Messaging { get; }
38
20 - public Intermediate Execute()
21 - {
22 -#if TODO_PATCHING
23 - Output output;
39 + private IBackendHelper BackendHelper { get; }
40 +
41 + private string DatabasePath { get; }
42 +
43 + private string ExportBasePath { get; }
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 + {
55 try
56 {
27 - using (Database database = new Database(this.Context.InputFilePath, OpenDatabase.ReadOnly))
57 + using (var database = new Database(this.DatabasePath, OpenDatabase.ReadOnly))
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);
30 - output = unbindCommand.Execute();
59 + var unbindCommand = new UnbindDatabaseCommand(this.Messaging, this.BackendHelper, database, this.DatabasePath, OutputType.Product, this.ExportBasePath, this.IntermediateFolder, this.IsAdminImage, this.SuppressDemodularization, skipSummaryInfo: false);
60 + var data = unbindCommand.Execute();
61
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)
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);
66 extractCommand.Execute();
67 }
68 +
69 + return data;
70 }
71 }
72 catch (Win32Exception e)
73 {
74 if (0x6E == e.NativeErrorCode) // ERROR_OPEN_FAILED
75 {
44 - throw new WixException(WixErrors.OpenDatabaseFailed(this.Context.InputFilePath));
76 + //throw new WixException(WixErrors.OpenDatabaseFailed(this.DatabasePath));
77 }
78
79 throw;
80 }
49 -
50 - return output;
51 -#endif
52 - throw new NotImplementedException();
81 }
82 }
83 }
src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindTransformCommand.cs renamed
+1 -1
@@ -41,7 +41,7 @@ namespace WixToolset.Core.WindowsInstaller.Unbind
41 private TableDefinitionCollection TableDefinitions { get; }
42
43 private string EmptyFile { get; set; }
44 -
44 +
45 public WindowsInstallerData Execute()
46 {
47 var transform = new WindowsInstallerData(new SourceLineNumber(this.TransformFile));
src/wix/WixToolset.Core.WindowsInstaller/WindowsInstallerBackendErrors.cs
+8 -5
@@ -2,14 +2,17 @@
2
3 namespace WixToolset.Core.WindowsInstaller
4 {
5 + using System;
6 using WixToolset.Data;
7
8 internal static class WindowsInstallerBackendErrors
9 {
9 - //public static Message ReplaceThisWithTheFirstError(SourceLineNumber sourceLineNumbers)
10 - //{
11 - // return Message(sourceLineNumbers, Ids.ReplaceThisWithTheFirstError, "format string", arg1, arg2);
12 - //}
10 + public static Message CannotLoadWixoutAsTransform(SourceLineNumber sourceLineNumbers, Exception exception)
11 + {
12 + var additionalDetail = exception == null ? String.Empty : ", detail: " + exception.Message;
13 +
14 + return Message(sourceLineNumbers, Ids.CannotLoadWixoutAsTransform, "Could not load wixout file as a transform{1}", additionalDetail);
15 + }
16
17 private static Message Message(SourceLineNumber sourceLineNumber, Ids id, string format, params object[] args)
18 {
@@ -18,7 +21,7 @@ namespace WixToolset.Core.WindowsInstaller
21
22 public enum Ids
23 {
21 - // ReplaceThisWithTheFirstError = 7500,
24 + CannotLoadWixoutAsTransform = 7500,
25 } // last available is 7999. 8000 is BurnBackendErrors.
26 }
27 }
src/wix/WixToolset.Core.WindowsInstaller/WindowsInstallerBackendFactory.cs
-5
@@ -37,11 +37,6 @@ namespace WixToolset.Core.WindowsInstaller
37 //case "patchcreation":
38 //case ".pcp":
39 // return new PatchCreationBackend();
40 -
41 - case "transform":
42 - case ".mst":
43 - backend = new MstBackend();
44 - return true;
40 }
41
42 backend = null;
src/wix/test/WixToolsetTest.CoreIntegration/TestData/Language/Package.en-us.wxl
+2 -2
@@ -1,7 +1,7 @@
1 <?xml version="1.0" encoding="utf-8"?>
2 <WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="en-US">
3
4 - <String Id="DowngradeError">A newer version of [ProductName] is already installed.</String>
5 - <String Id="FeatureTitle">MsiPackage</String>
4 + <String Id="DowngradeError">A newer version en-us of [ProductName] is already installed.</String>
5 + <String Id="FeatureTitle">MsiPackage en-us</String>
6
7 </WixLocalization>
src/wix/test/WixToolsetTest.CoreIntegration/TestData/Language/Package.ja-jp.wxl
+2 -2
@@ -1,7 +1,7 @@
1 <?xml version="1.0" encoding="utf-8"?>
2 <WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="ja-JP">
3
4 - <String Id="DowngradeError">A newer version of [ProductName] is already installed.</String>
5 - <String Id="FeatureTitle">MsiPackage</String>
4 + <String Id="DowngradeError">A newer version ja-jp of [ProductName] is already installed.</String>
5 + <String Id="FeatureTitle">MsiPackage ja-jp</String>
6
7 </WixLocalization>
src/wix/test/WixToolsetTest.CoreIntegration/TransformFixture.cs new
+143
@@ -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 +
3 +namespace 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 +}