From 91cd2d65121a163e625d2f029025123b0f8467d2 Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Sat, 5 Mar 2022 11:09:13 -0800 Subject: Implement "wix msi transform" Brings the functionality of torch into the WindowsInstallerBackend as the "transform" subcommand. Fixes 4602 --- .../Bind/BindTransformCommand.cs | 22 +- .../Bind/LoadTableDefinitionsCommand.cs | 6 +- .../CommandLine/TransformSubcommand.cs | 352 +++++++++++++++++++++ .../CommandLine/WindowsInstallerCommand.cs | 5 + src/wix/WixToolset.Core.WindowsInstaller/Differ.cs | 178 +++++------ .../WixToolset.Core.WindowsInstaller/MsiBackend.cs | 4 +- .../WixToolset.Core.WindowsInstaller/MsmBackend.cs | 4 +- .../WixToolset.Core.WindowsInstaller/MspBackend.cs | 11 +- .../WixToolset.Core.WindowsInstaller/MstBackend.cs | 39 --- .../Unbind/UnbindMsiOrMsmCommand.cs | 62 +++- .../Unbind/UnbindTranformCommand.cs | 309 ------------------ .../Unbind/UnbindTransformCommand.cs | 309 ++++++++++++++++++ .../WindowsInstallerBackendErrors.cs | 13 +- .../WindowsInstallerBackendFactory.cs | 5 - .../TestData/Language/Package.en-us.wxl | 4 +- .../TestData/Language/Package.ja-jp.wxl | 4 +- .../TransformFixture.cs | 143 +++++++++ 17 files changed, 961 insertions(+), 509 deletions(-) create mode 100644 src/wix/WixToolset.Core.WindowsInstaller/CommandLine/TransformSubcommand.cs delete mode 100644 src/wix/WixToolset.Core.WindowsInstaller/MstBackend.cs delete mode 100644 src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindTranformCommand.cs create mode 100644 src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindTransformCommand.cs create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TransformFixture.cs 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 if (this.Transform.TryGetTable("Property", out var propertyTable)) { - for (int i = propertyTable.Rows.Count - 1; i >= 0; i--) + for (var i = propertyTable.Rows.Count - 1; i >= 0; i--) { - Row row = propertyTable.Rows[i]; + var row = propertyTable.Rows[i]; + var id = row.FieldAsString(0); - if ("ProductCode" == (string)row[0] || "ProductLanguage" == (string)row[0] || "ProductVersion" == (string)row[0] || "UpgradeCode" == (string)row[0]) + if ("ProductCode" == id || "ProductLanguage" == id || "ProductVersion" == id) { propertyTable.Rows.RemoveAt(i); - - if ("UpgradeCode" == (string)row[0]) - { - updatedUpgradeCode = (string)row[1]; - } + } + else if ("UpgradeCode" == id) + { + updatedUpgradeCode = id; + propertyTable.Rows.RemoveAt(i); } } } @@ -383,11 +384,6 @@ namespace WixToolset.Core.WindowsInstaller.Bind } } - //foreach (BinderExtension extension in this.Extensions) - //{ - // extension.PostBind(this.Context); - //} - // Any errors encountered up to this point can cause errors during generation. if (this.Messaging.EncounteredError) { 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 @@ namespace WixToolset.Core.WindowsInstaller.Bind { - using System; using System.Collections.Generic; - using System.Globalization; using System.Linq; using WixToolset.Data; using WixToolset.Data.Symbols; @@ -32,9 +30,9 @@ namespace WixToolset.Core.WindowsInstaller.Bind public TableDefinitionCollection Execute() { var tableDefinitions = new TableDefinitionCollection(WindowsInstallerTableDefinitions.All); - var customColumnsById = this.Section.Symbols.OfType().ToDictionary(t => t.Id.Id); + var customColumnsById = this.Section?.Symbols.OfType().ToDictionary(t => t.Id.Id); - if (customColumnsById.Any()) + if (customColumnsById?.Any() == true) { foreach (var symbol in this.Section.Symbols.OfType()) { 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 @@ +// 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. + +namespace WixToolset.Core.WindowsInstaller.CommandLine +{ + using System; + using System.IO; + using System.Threading; + using System.Threading.Tasks; + using WixToolset.Core.WindowsInstaller.Bind; + using WixToolset.Core.WindowsInstaller.Unbind; + using WixToolset.Data; + using WixToolset.Data.Symbols; + using WixToolset.Data.WindowsInstaller; + using WixToolset.Extensibility; + using WixToolset.Extensibility.Services; + + internal class TransformSubcommand : WindowsInstallerSubcommandBase + { + public TransformSubcommand(IServiceProvider serviceProvider) + { + this.Messaging = serviceProvider.GetService(); + this.BackendHelper = serviceProvider.GetService(); + this.ExtensionManager = serviceProvider.GetService(); + } + + private IMessaging Messaging { get; } + + private IBackendHelper BackendHelper { get; } + + private IExtensionManager ExtensionManager { get; } + + private string OutputPath { get; set; } + + private string TargetPath { get; set; } + + private string UpdatedPath { get; set; } + + private string ExportBasePath { get; set; } + + private string IntermediateFolder { get; set; } + + private bool IsAdminImage { get; set; } + + private bool PreserveUnchangedRows { get; set; } + + private bool ShowPedanticMessages { get; set; } + + private bool SuppressKeepingSpecialRows { get; set; } + + private bool OutputAsWixout { get; set; } + + private TransformFlags ValidationFlags { get; set; } + + public override Task ExecuteAsync(CancellationToken cancellationToken) + { + if (String.IsNullOrEmpty(this.TargetPath)) + { + Console.Error.WriteLine("Input file required"); + return Task.FromResult(-1); + } + + if (String.IsNullOrEmpty(this.OutputPath)) + { + Console.Error.WriteLine("Output file required"); + return Task.FromResult(-1); + } + + if (String.IsNullOrEmpty(this.IntermediateFolder)) + { + this.IntermediateFolder = Path.GetTempPath(); + } + + var transform = this.LoadTransform(); + + if (!this.Messaging.EncounteredError) + { + this.SaveTransform(transform); + } + + return Task.FromResult(this.Messaging.EncounteredError ? 1 : 0); + } + + public override bool TryParseArgument(ICommandLineParser parser, string argument) + { + if (parser.IsSwitch(argument)) + { + var parameter = argument.Substring(1); + switch (parameter.ToLowerInvariant()) + { + case "a": + this.IsAdminImage = true; + return true; + + case "intermediatefolder": + this.IntermediateFolder = parser.GetNextArgumentAsDirectoryOrError(argument); + return true; + + case "o": + case "out": + this.OutputPath = parser.GetNextArgumentAsFilePathOrError(argument); + return true; + + case "p": + this.PreserveUnchangedRows = true; + return true; + + case "pedantic": + this.ShowPedanticMessages = true; + return true; + + case "serr": + { + var serr = parser.GetNextArgumentOrError(argument); + + switch (serr.ToLowerInvariant()) + { + case "a": + this.ValidationFlags |= TransformFlags.ErrorAddExistingRow; + return true; + + case "b": + this.ValidationFlags |= TransformFlags.ErrorDeleteMissingRow; + return true; + + case "c": + this.ValidationFlags |= TransformFlags.ErrorAddExistingTable; + return true; + + case "d": + this.ValidationFlags |= TransformFlags.ErrorDeleteMissingTable; + return true; + + case "e": + this.ValidationFlags |= TransformFlags.ErrorUpdateMissingRow; + return true; + + case "f": + this.ValidationFlags |= TransformFlags.ErrorChangeCodePage; + return true; + + default: + this.Messaging.Write(ErrorMessages.ExpectedArgument(serr)); + return true; + } + } + + case "val": + { + var val = parser.GetNextArgumentOrError(argument); + + switch (val.ToLowerInvariant()) + { + case "language": + this.ValidationFlags |= TransformFlags.LanguageTransformDefault; + return true; + + case "instance": + this.ValidationFlags |= TransformFlags.InstanceTransformDefault; + return true; + + case "patch": + this.ValidationFlags |= TransformFlags.PatchTransformDefault; + return true; + + case "g": + this.ValidationFlags |= TransformFlags.ValidateUpgradeCode; + return true; + + case "l": + this.ValidationFlags |= TransformFlags.ValidateLanguage; + return true; + + case "r": + this.ValidationFlags |= TransformFlags.ValidateProduct; + return true; + + case "s": + this.ValidationFlags |= TransformFlags.ValidateMajorVersion; + return true; + + case "t": + this.ValidationFlags |= TransformFlags.ValidateMinorVersion; + return true; + + case "u": + this.ValidationFlags |= TransformFlags.ValidateUpdateVersion; + return true; + + case "v": + this.ValidationFlags |= TransformFlags.ValidateNewLessBaseVersion; + return true; + + case "w": + this.ValidationFlags |= TransformFlags.ValidateNewLessEqualBaseVersion; + return true; + + case "x": + this.ValidationFlags |= TransformFlags.ValidateNewEqualBaseVersion; + return true; + + case "y": + this.ValidationFlags |= TransformFlags.ValidateNewGreaterEqualBaseVersion; + return true; + + case "z": + this.ValidationFlags |= TransformFlags.ValidateNewGreaterBaseVersion; + return true; + + default: + this.Messaging.Write(ErrorMessages.ExpectedArgument(val)); + return true; + } + } + + case "x": + this.ExportBasePath = parser.GetNextArgumentAsDirectoryOrError(argument); + return true; + + case "xo": + this.OutputAsWixout = true; + return true; + } + } + else if (String.IsNullOrEmpty(this.TargetPath)) + { + this.TargetPath = argument; + return true; + } + else if (String.IsNullOrEmpty(this.UpdatedPath)) + { + this.UpdatedPath = argument; + return true; + } + + return false; + } + + private WindowsInstallerData LoadTransform() + { + WindowsInstallerData transform; + + if (String.IsNullOrEmpty(this.UpdatedPath)) + { + Exception exception; + + (transform, exception) = LoadWindowsInstallerDataSafely(this.TargetPath); + + if (transform?.Type != OutputType.Transform) + { + this.Messaging.Write(WindowsInstallerBackendErrors.CannotLoadWixoutAsTransform(new SourceLineNumber(this.TargetPath), exception)); + } + } + else + { + transform = this.CreateTransform(); + + if (null == transform.Tables || 0 >= transform.Tables.Count) + { + this.Messaging.Write(ErrorMessages.NoDifferencesInTransform(new SourceLineNumber(this.OutputPath))); + } + } + + return transform; + } + + private void SaveTransform(WindowsInstallerData transform) + { + if (this.OutputAsWixout) + { + using (var output = WixOutput.Create(this.OutputPath)) + { + transform.Save(output); + } + } + else + { + var fileSystemExtensions = this.ExtensionManager.GetServices(); + var fileSystemManager = new FileSystemManager(fileSystemExtensions); + + var tableDefinitions = this.GetTableDefinitions(); + + var bindCommand = new BindTransformCommand(this.Messaging, this.BackendHelper, fileSystemManager, this.IntermediateFolder, transform, this.OutputPath, tableDefinitions); + bindCommand.Execute(); + } + } + + private WindowsInstallerData CreateTransform() + { + if (!TryLoadWindowsInstallerData(this.TargetPath, out var targetOutput)) + { + var unbindCommand = new UnbindMsiOrMsmCommand(this.Messaging, this.BackendHelper, this.TargetPath, this.ExportBasePath, this.IntermediateFolder, this.IsAdminImage, suppressDemodularization: true, suppressExtractCabinets: true); + targetOutput = unbindCommand.Execute(); + } + + if (!TryLoadWindowsInstallerData(this.TargetPath, out var updatedOutput)) + { + var unbindCommand = new UnbindMsiOrMsmCommand(this.Messaging, this.BackendHelper, this.UpdatedPath, this.ExportBasePath, this.IntermediateFolder, this.IsAdminImage, suppressDemodularization: true, suppressExtractCabinets: true); + updatedOutput = unbindCommand.Execute(); + } + + var differ = new Differ(this.Messaging) + { + PreserveUnchangedRows = this.PreserveUnchangedRows, + ShowPedanticMessages = this.ShowPedanticMessages, + SuppressKeepingSpecialRows = this.SuppressKeepingSpecialRows + }; + + return differ.Diff(targetOutput, updatedOutput, this.ValidationFlags); + } + + private TableDefinitionCollection GetTableDefinitions() + { + var backendExtensions = this.ExtensionManager.GetServices(); + + var loadTableDefinitions = new LoadTableDefinitionsCommand(this.Messaging, null, backendExtensions); + return loadTableDefinitions.Execute(); + } + + private static bool TryLoadWindowsInstallerData(string path, out WindowsInstallerData data) + { + data = null; + + var extension = Path.GetExtension(path); + + // If the path is _not_ obviously a Windows Installer database, let's try opening it as + // our own data file format. + if (!extension.Equals(".msi", StringComparison.OrdinalIgnoreCase) && !extension.Equals(".msm", StringComparison.OrdinalIgnoreCase)) + { + (data, _) = LoadWindowsInstallerDataSafely(path); + } + + return data != null; + } + + private static (WindowsInstallerData, Exception) LoadWindowsInstallerDataSafely(string path) + { + WindowsInstallerData data = null; + Exception exception = null; + + try + { + data = WindowsInstallerData.Load(path); + } + catch (Exception e) + { + exception = e; + } + + return (data, exception); + } + } +} 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 this.Subcommand = new InscribeSubcommand(this.ServiceProvider); return true; + case "transform": + this.Subcommand = new TransformSubcommand(this.ServiceProvider); + return true; + case "validate": this.Subcommand = new ValidateSubcommand(this.ServiceProvider); return true; @@ -72,6 +76,7 @@ namespace WixToolset.Core.WindowsInstaller.CommandLine Console.WriteLine("Commands:"); Console.WriteLine(); Console.WriteLine(" inscribe Updates MSI database with cabinet signature information."); + Console.WriteLine(" transform Creates an MST transform file."); Console.WriteLine(" validate Validates MSI database using standard or custom ICEs."); } } 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 @@ // 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. -#if DELETE - namespace WixToolset.Core.WindowsInstaller { using System; using System.Collections; using System.Collections.Generic; using System.Globalization; - using WixToolset.Core.WindowsInstaller.Msi; + using WixToolset.Core.Native.Msi; using WixToolset.Data; using WixToolset.Data.Symbols; using WixToolset.Data.WindowsInstaller; - using WixToolset.Data.WindowsInstaller.Rows; using WixToolset.Extensibility; using WixToolset.Extensibility.Services; @@ -22,9 +19,6 @@ namespace WixToolset.Core.WindowsInstaller public sealed class Differ { private readonly List inspectorExtensions; - private bool showPedanticMessages; - private bool suppressKeepingSpecialRows; - private bool preserveUnchangedRows; private const char sectionDelimiter = '/'; private readonly IMessaging messaging; private SummaryInformationStreams transformSummaryInfo; @@ -42,31 +36,19 @@ namespace WixToolset.Core.WindowsInstaller /// Gets or sets the option to show pedantic messages. /// /// The option to show pedantic messages. - public bool ShowPedanticMessages - { - get { return this.showPedanticMessages; } - set { this.showPedanticMessages = value; } - } + public bool ShowPedanticMessages { get; set; } /// /// Gets or sets the option to suppress keeping special rows. /// /// The option to suppress keeping special rows. - public bool SuppressKeepingSpecialRows - { - get { return this.suppressKeepingSpecialRows; } - set { this.suppressKeepingSpecialRows = value; } - } + public bool SuppressKeepingSpecialRows { get; set; } /// /// Gets or sets the flag to determine if all rows, even unchanged ones will be persisted in the output. /// /// The option to keep all rows including unchanged rows. - public bool PreserveUnchangedRows - { - get { return this.preserveUnchangedRows; } - set { this.preserveUnchangedRows = value; } - } + public bool PreserveUnchangedRows { get; set; } /// /// Adds an extension. @@ -97,7 +79,7 @@ namespace WixToolset.Core.WindowsInstaller /// The transform. public WindowsInstallerData Diff(WindowsInstallerData targetOutput, WindowsInstallerData updatedOutput, TransformFlags validationFlags) { - WindowsInstallerData transform = new WindowsInstallerData(null); + var transform = new WindowsInstallerData(null); transform.Type = OutputType.Transform; transform.Codepage = updatedOutput.Codepage; this.transformSummaryInfo = new SummaryInformationStreams(); @@ -119,34 +101,34 @@ namespace WixToolset.Core.WindowsInstaller } // compare the contents of the tables - foreach (Table targetTable in targetOutput.Tables) + foreach (var targetTable in targetOutput.Tables) { - Table updatedTable = updatedOutput.Tables[targetTable.Name]; - TableOperation operation = TableOperation.None; + var updatedTable = updatedOutput.Tables[targetTable.Name]; + var operation = TableOperation.None; - List rows = this.CompareTables(targetOutput, targetTable, updatedTable, out operation); + var rows = this.CompareTables(targetOutput, targetTable, updatedTable, out operation); if (TableOperation.Drop == operation) { - Table droppedTable = transform.EnsureTable(targetTable.Definition); + var droppedTable = transform.EnsureTable(targetTable.Definition); droppedTable.Operation = TableOperation.Drop; } else if (TableOperation.None == operation) { - Table modified = transform.EnsureTable(updatedTable.Definition); + var modified = transform.EnsureTable(updatedTable.Definition); rows.ForEach(r => modified.Rows.Add(r)); } } // added tables - foreach (Table updatedTable in updatedOutput.Tables) + foreach (var updatedTable in updatedOutput.Tables) { if (null == targetOutput.Tables[updatedTable.Name]) { - Table addedTable = transform.EnsureTable(updatedTable.Definition); + var addedTable = transform.EnsureTable(updatedTable.Definition); addedTable.Operation = TableOperation.Add; - foreach (Row updatedRow in updatedTable.Rows) + foreach (var updatedRow in updatedTable.Rows) { updatedRow.Operation = RowOperation.Add; updatedRow.SectionId = sectionDelimiter + updatedRow.SectionId; @@ -156,9 +138,9 @@ namespace WixToolset.Core.WindowsInstaller } // set summary information properties - if (!this.suppressKeepingSpecialRows) + if (!this.SuppressKeepingSpecialRows) { - Table summaryInfoTable = transform.Tables["_SummaryInformation"]; + var summaryInfoTable = transform.Tables["_SummaryInformation"]; this.UpdateTransformSummaryInformationTable(summaryInfoTable, validationFlags); } @@ -170,53 +152,56 @@ namespace WixToolset.Core.WindowsInstaller /// /// The indexed rows. /// The row to index. - private void AddIndexedRow(IDictionary index, Row row) + private void AddIndexedRow(IDictionary index, Row row) { - string primaryKey = row.GetPrimaryKey('/'); - if (null != primaryKey) + var primaryKey = row.GetPrimaryKey('/'); + + // If there is no primary, use the string representation of the row as its + // primary key (even though it may not be unique). + if (String.IsNullOrEmpty(primaryKey)) { - // Overriding WixActionRows have a primary key defined and take precedence in the index. - if (row is WixActionRow) + // This is provided for compatibility with unreal tables with no primary key + // all real tables must specify at least one column as the primary key. + primaryKey = row.ToString(); + index[primaryKey] = row; + } + else + { + if (!index.TryGetValue(primaryKey, out var existingRow)) + { + index.Add(primaryKey, row); + } + else { - WixActionRow currentRow = (WixActionRow)row; - if (index.Contains(primaryKey)) +#if TODO + // Overriding WixActionRows have a primary key defined and take precedence in the index. + if (row is WixActionRow currentActionRow) { // If the current row is not overridable, see if the indexed row is. - if (!currentRow.Overridable) + if (!currentActionRow.Overridable) { - WixActionRow indexedRow = index[primaryKey] as WixActionRow; - if (null != indexedRow && indexedRow.Overridable) + if (existingRow is WixActionRow existingActionRow && existingActionRow.Overridable) { // The indexed key is overridable and should be replaced // (not removed and re-added which results in two Array.Copy // operations for SortedList, or may be re-hashing in other // implementations of IDictionary). - index[primaryKey] = currentRow; + index[primaryKey] = currentActionRow; } } // If we got this far, the row does not need to be indexed. return; } - } +#endif - // Nothing else should be added more than once. - if (!index.Contains(primaryKey)) - { - index.Add(primaryKey, row); - } - else if (this.showPedanticMessages) - { - this.messaging.Write(ErrorMessages.DuplicatePrimaryKey(row.SourceLineNumbers, primaryKey, row.Table.Name)); + // Nothing else should be added more than once. + if (this.ShowPedanticMessages) + { + this.messaging.Write(ErrorMessages.DuplicatePrimaryKey(row.SourceLineNumbers, primaryKey, row.Table.Name)); + } } } - else // use the string representation of the row as its primary key (it may not be unique) - { - // this is provided for compatibility with unreal tables with no primary key - // all real tables must specify at least one column as the primary key - primaryKey = row.ToString(); - index[primaryKey] = row; - } } private Row CompareRows(Table targetTable, Row targetRow, Row updatedRow, out RowOperation operation, out bool keepRow) @@ -235,7 +220,7 @@ namespace WixToolset.Core.WindowsInstaller else if (null == updatedRow) { operation = targetRow.Operation = RowOperation.Delete; - targetRow.SectionId = targetRow.SectionId + sectionDelimiter; + targetRow.SectionId += sectionDelimiter; comparedRow = targetRow; keepRow = true; } @@ -243,7 +228,7 @@ namespace WixToolset.Core.WindowsInstaller else // possibly modified { updatedRow.Operation = RowOperation.None; - if (!this.suppressKeepingSpecialRows && "_SummaryInformation" == targetTable.Name) + if (!this.SuppressKeepingSpecialRows && "_SummaryInformation" == targetTable.Name) { // ignore rows that shouldn't be in a transform if (Enum.IsDefined(typeof(SummaryInformation.Transform), (int)updatedRow[0])) @@ -256,18 +241,18 @@ namespace WixToolset.Core.WindowsInstaller } else { - if (this.preserveUnchangedRows) + if (this.PreserveUnchangedRows) { keepRow = true; } - for (int i = 0; i < updatedRow.Fields.Length; i++) + for (var i = 0; i < updatedRow.Fields.Length; i++) { - ColumnDefinition columnDefinition = updatedRow.Fields[i].Column; + var columnDefinition = updatedRow.Fields[i].Column; if (!columnDefinition.PrimaryKey) { - bool modified = false; + var modified = false; if (i >= targetRow.Fields.Length) { @@ -290,12 +275,12 @@ namespace WixToolset.Core.WindowsInstaller updatedRow.Fields[i].PreviousData = (string)targetRow.Fields[i].Data; // keep rows containing preserved fields so the historical data is available to the binder - keepRow = !this.suppressKeepingSpecialRows; + keepRow = !this.SuppressKeepingSpecialRows; } else if (ColumnType.Object == columnDefinition.Type) { - ObjectField targetObjectField = (ObjectField)targetRow.Fields[i]; - ObjectField updatedObjectField = (ObjectField)updatedRow.Fields[i]; + var targetObjectField = (ObjectField)targetRow.Fields[i]; + var updatedObjectField = (ObjectField)updatedRow.Fields[i]; updatedObjectField.PreviousEmbeddedFileIndex = targetObjectField.EmbeddedFileIndex; updatedObjectField.PreviousBaseUri = targetObjectField.BaseUri; @@ -308,7 +293,7 @@ namespace WixToolset.Core.WindowsInstaller updatedObjectField.UnresolvedPreviousData = (string)targetObjectField.UnresolvedData; // keep rows containing object fields so the files can be compared in the binder - keepRow = !this.suppressKeepingSpecialRows; + keepRow = !this.SuppressKeepingSpecialRows; } else { @@ -342,7 +327,7 @@ namespace WixToolset.Core.WindowsInstaller private List CompareTables(WindowsInstallerData targetOutput, Table targetTable, Table updatedTable, out TableOperation operation) { - List rows = new List(); + var rows = new List(); operation = TableOperation.None; // dropped tables @@ -360,8 +345,8 @@ namespace WixToolset.Core.WindowsInstaller } else // possibly modified tables { - SortedList updatedPrimaryKeys = new SortedList(); - SortedList targetPrimaryKeys = new SortedList(); + var updatedPrimaryKeys = new SortedDictionary(); + var targetPrimaryKeys = new SortedDictionary(); // compare the table definitions if (0 != targetTable.Definition.CompareTo(updatedTable.Definition)) @@ -374,13 +359,10 @@ namespace WixToolset.Core.WindowsInstaller this.IndexPrimaryKeys(targetTable, targetPrimaryKeys, updatedTable, updatedPrimaryKeys); // diff the target and updated rows - foreach (DictionaryEntry targetPrimaryKeyEntry in targetPrimaryKeys) + foreach (var targetPrimaryKeyEntry in targetPrimaryKeys) { - string targetPrimaryKey = (string)targetPrimaryKeyEntry.Key; - bool keepRow = false; - RowOperation rowOperation = RowOperation.None; - - Row compared = this.CompareRows(targetTable, targetPrimaryKeyEntry.Value as Row, updatedPrimaryKeys[targetPrimaryKey] as Row, out rowOperation, out keepRow); + var targetPrimaryKey = targetPrimaryKeyEntry.Key; + var compared = this.CompareRows(targetTable, targetPrimaryKeyEntry.Value, updatedPrimaryKeys[targetPrimaryKey], out var _, out var keepRow); if (keepRow) { @@ -389,13 +371,13 @@ namespace WixToolset.Core.WindowsInstaller } // find the inserted rows - foreach (DictionaryEntry updatedPrimaryKeyEntry in updatedPrimaryKeys) + foreach (var updatedPrimaryKeyEntry in updatedPrimaryKeys) { - string updatedPrimaryKey = (string)updatedPrimaryKeyEntry.Key; + var updatedPrimaryKey = (string)updatedPrimaryKeyEntry.Key; - if (!targetPrimaryKeys.Contains(updatedPrimaryKey)) + if (!targetPrimaryKeys.ContainsKey(updatedPrimaryKey)) { - Row updatedRow = (Row)updatedPrimaryKeyEntry.Value; + var updatedRow = (Row)updatedPrimaryKeyEntry.Value; updatedRow.Operation = RowOperation.Add; updatedRow.SectionId = sectionDelimiter + updatedRow.SectionId; @@ -408,10 +390,10 @@ namespace WixToolset.Core.WindowsInstaller return rows; } - private void IndexPrimaryKeys(Table targetTable, SortedList targetPrimaryKeys, Table updatedTable, SortedList updatedPrimaryKeys) + private void IndexPrimaryKeys(Table targetTable, SortedDictionary targetPrimaryKeys, Table updatedTable, SortedDictionary updatedPrimaryKeys) { // index the target rows - foreach (Row row in targetTable.Rows) + foreach (var row in targetTable.Rows) { this.AddIndexedRow(targetPrimaryKeys, row); @@ -452,7 +434,7 @@ namespace WixToolset.Core.WindowsInstaller } // index the updated rows - foreach (Row row in updatedTable.Rows) + foreach (var row in updatedTable.Rows) { this.AddIndexedRow(updatedPrimaryKeys, row); @@ -492,17 +474,15 @@ namespace WixToolset.Core.WindowsInstaller private void UpdateTransformSummaryInformationTable(Table summaryInfoTable, TransformFlags validationFlags) { // calculate the minimum version of MSI required to process the transform - int targetMin; - int updatedMin; - int minimumVersion = 100; + var minimumVersion = 100; - if (Int32.TryParse(this.transformSummaryInfo.TargetMinimumVersion, out targetMin) && Int32.TryParse(this.transformSummaryInfo.UpdatedMinimumVersion, out updatedMin)) + if (Int32.TryParse(this.transformSummaryInfo.TargetMinimumVersion, out var targetMin) && Int32.TryParse(this.transformSummaryInfo.UpdatedMinimumVersion, out var updatedMin)) { minimumVersion = Math.Max(targetMin, updatedMin); } - Hashtable summaryRows = new Hashtable(summaryInfoTable.Rows.Count); - foreach (Row row in summaryInfoTable.Rows) + var summaryRows = new Hashtable(summaryInfoTable.Rows.Count); + foreach (var row in summaryInfoTable.Rows) { summaryRows[row[0]] = row; @@ -535,35 +515,35 @@ namespace WixToolset.Core.WindowsInstaller if (!summaryRows.Contains((int)SummaryInformation.Transform.TargetPlatformAndLanguage)) { - Row summaryRow = summaryInfoTable.CreateRow(null); + var summaryRow = summaryInfoTable.CreateRow(null); summaryRow[0] = (int)SummaryInformation.Transform.TargetPlatformAndLanguage; summaryRow[1] = this.transformSummaryInfo.TargetPlatformAndLanguage; } if (!summaryRows.Contains((int)SummaryInformation.Transform.UpdatedPlatformAndLanguage)) { - Row summaryRow = summaryInfoTable.CreateRow(null); + var summaryRow = summaryInfoTable.CreateRow(null); summaryRow[0] = (int)SummaryInformation.Transform.UpdatedPlatformAndLanguage; summaryRow[1] = this.transformSummaryInfo.UpdatedPlatformAndLanguage; } if (!summaryRows.Contains((int)SummaryInformation.Transform.ValidationFlags)) { - Row summaryRow = summaryInfoTable.CreateRow(null); + var summaryRow = summaryInfoTable.CreateRow(null); summaryRow[0] = (int)SummaryInformation.Transform.ValidationFlags; summaryRow[1] = ((int)validationFlags).ToString(CultureInfo.InvariantCulture); } if (!summaryRows.Contains((int)SummaryInformation.Transform.InstallerRequirement)) { - Row summaryRow = summaryInfoTable.CreateRow(null); + var summaryRow = summaryInfoTable.CreateRow(null); summaryRow[0] = (int)SummaryInformation.Transform.InstallerRequirement; summaryRow[1] = minimumVersion.ToString(CultureInfo.InvariantCulture); } if (!summaryRows.Contains((int)SummaryInformation.Transform.Security)) { - Row summaryRow = summaryInfoTable.CreateRow(null); + var summaryRow = summaryInfoTable.CreateRow(null); summaryRow[0] = (int)SummaryInformation.Transform.Security; summaryRow[1] = "4"; } @@ -606,5 +586,3 @@ namespace WixToolset.Core.WindowsInstaller } } } - -#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 @@ namespace WixToolset.Core.WindowsInstaller { + using System; using WixToolset.Core.WindowsInstaller.Bind; using WixToolset.Core.WindowsInstaller.Decompile; using WixToolset.Core.WindowsInstaller.Unbind; @@ -71,8 +72,7 @@ namespace WixToolset.Core.WindowsInstaller public Intermediate Unbind(IUnbindContext context) { - var command = new UnbindMsiOrMsmCommand(context); - return command.Execute(); + throw new NotImplementedException(); } } } 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 @@ namespace WixToolset.Core.WindowsInstaller { + using System; using WixToolset.Core.WindowsInstaller.Bind; using WixToolset.Core.WindowsInstaller.Decompile; using WixToolset.Core.WindowsInstaller.Unbind; @@ -67,8 +68,7 @@ namespace WixToolset.Core.WindowsInstaller public Intermediate Unbind(IUnbindContext context) { - var command = new UnbindMsiOrMsmCommand(context); - return command.Execute(); + throw new NotImplementedException(); } } } 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 { using System; using System.Collections.Generic; - using System.IO; - using System.Linq; using WixToolset.Core.WindowsInstaller.Bind; - using WixToolset.Core.Native.Msi; - using WixToolset.Core.WindowsInstaller.Unbind; - using WixToolset.Data; - using WixToolset.Data.Symbols; using WixToolset.Data.WindowsInstaller; using WixToolset.Extensibility; using WixToolset.Extensibility.Data; @@ -74,9 +68,9 @@ namespace WixToolset.Core.WindowsInstaller throw new NotImplementedException(); } +#if TODO_PATCHING public Intermediate Unbind(IUnbindContext context) { -#if TODO_PATCHING Output patch; // 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 } return patch; -#endif - throw new NotImplementedException(); } +#endif } } 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 @@ -// 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. - -namespace WixToolset.Core.WindowsInstaller -{ - using System; - using WixToolset.Core.WindowsInstaller.Unbind; - using WixToolset.Data; - using WixToolset.Extensibility; - using WixToolset.Extensibility.Data; - - internal class MstBackend : IBackend - { - public IBindResult Bind(IBindContext context) - { -#if TODO_PATCHING - var command = new BindTransformCommand(); - command.Extensions = context.Extensions; - command.TempFilesLocation = context.IntermediateFolder; - command.Transform = context.IntermediateRepresentation; - command.OutputPath = context.OutputPath; - command.Execute(); - - return new BindResult(Array.Empty(), Array.Empty()); -#endif - throw new NotImplementedException(); - } - - public IDecompileResult Decompile(IDecompileContext context) - { - throw new NotImplementedException(); - } - - public Intermediate Unbind(IUnbindContext context) - { - var command = new UnbindMsiOrMsmCommand(context); - return command.Execute(); - } - } -} 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 { using System; using System.ComponentModel; + using WixToolset.Core.Native.Msi; using WixToolset.Data; + using WixToolset.Data.WindowsInstaller; using WixToolset.Extensibility.Data; - using WixToolset.Core.Native.Msi; + using WixToolset.Extensibility.Services; internal class UnbindMsiOrMsmCommand { + public UnbindMsiOrMsmCommand(IMessaging messaging, IBackendHelper backendHelper, string databasePath, string exportBasePath, string intermediateFolder, bool adminImage, bool suppressDemodularization, bool suppressExtractCabinets) + { + this.Messaging = messaging; + this.BackendHelper = backendHelper; + this.DatabasePath = databasePath; + this.ExportBasePath = exportBasePath; + this.IntermediateFolder = intermediateFolder; + this.IsAdminImage = adminImage; + this.SuppressDemodularization = suppressDemodularization; + this.SuppressExtractCabinets = suppressExtractCabinets; + } + public UnbindMsiOrMsmCommand(IUnbindContext context) { - this.Context = context; + this.Messaging = context.ServiceProvider.GetService(); + this.DatabasePath = context.InputFilePath; + this.ExportBasePath = context.ExportBasePath; + this.IntermediateFolder = context.IntermediateFolder; + this.IsAdminImage = context.IsAdminImage; + this.SuppressDemodularization = context.SuppressDemodularization; } - public IUnbindContext Context { get; } + private IMessaging Messaging { get; } - public Intermediate Execute() - { -#if TODO_PATCHING - Output output; + private IBackendHelper BackendHelper { get; } + + private string DatabasePath { get; } + + private string ExportBasePath { get; } + private string IntermediateFolder { get; } + + private bool IsAdminImage { get; } + + private bool SuppressDemodularization { get; } + + private bool SuppressExtractCabinets { get; } + + public WindowsInstallerData Execute() + { try { - using (Database database = new Database(this.Context.InputFilePath, OpenDatabase.ReadOnly)) + using (var database = new Database(this.DatabasePath, OpenDatabase.ReadOnly)) { - 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); - output = unbindCommand.Execute(); + var unbindCommand = new UnbindDatabaseCommand(this.Messaging, this.BackendHelper, database, this.DatabasePath, OutputType.Product, this.ExportBasePath, this.IntermediateFolder, this.IsAdminImage, this.SuppressDemodularization, skipSummaryInfo: false); + var data = unbindCommand.Execute(); // extract the files from the cabinets - if (!String.IsNullOrEmpty(this.Context.ExportBasePath) && !this.Context.SuppressExtractCabinets) + if (!String.IsNullOrEmpty(this.ExportBasePath) && !this.SuppressExtractCabinets) { - var extractCommand = new ExtractCabinetsCommand(output, database, this.Context.InputFilePath, this.Context.ExportBasePath, this.Context.IntermediateFolder); + var extractCommand = new ExtractCabinetsCommand(data, database, this.DatabasePath, this.ExportBasePath, this.IntermediateFolder); extractCommand.Execute(); } + + return data; } } catch (Win32Exception e) { if (0x6E == e.NativeErrorCode) // ERROR_OPEN_FAILED { - throw new WixException(WixErrors.OpenDatabaseFailed(this.Context.InputFilePath)); + //throw new WixException(WixErrors.OpenDatabaseFailed(this.DatabasePath)); } throw; } - - return output; -#endif - throw new NotImplementedException(); } } } diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindTranformCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindTranformCommand.cs deleted file mode 100644 index f40aed4e..00000000 --- a/src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindTranformCommand.cs +++ /dev/null @@ -1,309 +0,0 @@ -// 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. - -namespace WixToolset.Core.WindowsInstaller.Unbind -{ - using System; - using System.Collections; - using System.Collections.Generic; - using System.ComponentModel; - using System.Globalization; - using System.IO; - using System.Linq; - using WixToolset.Core.Native.Msi; - using WixToolset.Core.WindowsInstaller.Bind; - using WixToolset.Data; - using WixToolset.Data.WindowsInstaller; - using WixToolset.Extensibility.Services; - - internal class UnbindTransformCommand - { - public UnbindTransformCommand(IMessaging messaging, IBackendHelper backendHelper, string transformFile, string exportBasePath, string intermediateFolder) - { - this.Messaging = messaging; - this.BackendHelper = backendHelper; - this.TransformFile = transformFile; - this.ExportBasePath = exportBasePath; - this.IntermediateFolder = intermediateFolder; - - this.TableDefinitions = new TableDefinitionCollection(WindowsInstallerTableDefinitions.All); - } - - private IMessaging Messaging { get; } - - private IBackendHelper BackendHelper { get; } - - private string TransformFile { get; } - - private string ExportBasePath { get; } - - private string IntermediateFolder { get; } - - private TableDefinitionCollection TableDefinitions { get; } - - private string EmptyFile { get; set; } - - public WindowsInstallerData Execute() - { - var transform = new WindowsInstallerData(new SourceLineNumber(this.TransformFile)); - transform.Type = OutputType.Transform; - - // get the summary information table - using (var summaryInformation = new SummaryInformation(this.TransformFile)) - { - var table = transform.EnsureTable(this.TableDefinitions["_SummaryInformation"]); - - for (var i = 1; 19 >= i; i++) - { - var value = summaryInformation.GetProperty(i); - - if (0 < value.Length) - { - var row = table.CreateRow(transform.SourceLineNumbers); - row[0] = i; - row[1] = value; - } - } - } - - // create a schema msi which hopefully matches the table schemas in the transform - var schemaOutput = new WindowsInstallerData(null); - var msiDatabaseFile = Path.Combine(this.IntermediateFolder, "schema.msi"); - foreach (var tableDefinition in this.TableDefinitions) - { - // skip unreal tables and the Patch table - if (!tableDefinition.Unreal && "Patch" != tableDefinition.Name) - { - schemaOutput.EnsureTable(tableDefinition); - } - } - - var addedRows = new Dictionary(); - Table transformViewTable; - - // Bind the schema msi. - this.GenerateDatabase(schemaOutput, msiDatabaseFile); - - // apply the transform to the database and retrieve the modifications - using (var msiDatabase = new Database(msiDatabaseFile, OpenDatabase.Transact)) - { - // apply the transform with the ViewTransform option to collect all the modifications - msiDatabase.ApplyTransform(this.TransformFile, TransformErrorConditions.All | TransformErrorConditions.ViewTransform); - - // unbind the database - var unbindCommand = new UnbindDatabaseCommand(this.Messaging, this.BackendHelper, msiDatabase, msiDatabaseFile, OutputType.Product, this.ExportBasePath, this.IntermediateFolder, false, false, skipSummaryInfo: true); - var transformViewOutput = unbindCommand.Execute(); - - // index the added and possibly modified rows (added rows may also appears as modified rows) - transformViewTable = transformViewOutput.Tables["_TransformView"]; - var modifiedRows = new Hashtable(); - foreach (var row in transformViewTable.Rows) - { - var tableName = (string)row[0]; - var columnName = (string)row[1]; - var primaryKeys = (string)row[2]; - - if ("INSERT" == columnName) - { - var index = String.Concat(tableName, ':', primaryKeys); - - addedRows.Add(index, null); - } - else if ("CREATE" != columnName && "DELETE" != columnName && "DROP" != columnName && null != primaryKeys) // modified row - { - var index = String.Concat(tableName, ':', primaryKeys); - - modifiedRows[index] = row; - } - } - - // create placeholder rows for modified rows to make the transform insert the updated values when its applied - foreach (Row row in modifiedRows.Values) - { - var tableName = (string)row[0]; - var columnName = (string)row[1]; - var primaryKeys = (string)row[2]; - - var index = String.Concat(tableName, ':', primaryKeys); - - // ignore information for added rows - if (!addedRows.ContainsKey(index)) - { - var table = schemaOutput.Tables[tableName]; - this.CreateRow(table, primaryKeys, true); - } - } - } - - // Re-bind the schema output with the placeholder rows. - this.GenerateDatabase(schemaOutput, msiDatabaseFile); - - // apply the transform to the database and retrieve the modifications - using (var msiDatabase = new Database(msiDatabaseFile, OpenDatabase.Transact)) - { - try - { - // apply the transform - msiDatabase.ApplyTransform(this.TransformFile, TransformErrorConditions.All); - - // commit the database to guard against weird errors with streams - msiDatabase.Commit(); - } - catch (Win32Exception ex) - { - if (0x65B == ex.NativeErrorCode) - { - // this commonly happens when the transform was built - // against a database schema different from the internal - // table definitions - throw new WixException(ErrorMessages.TransformSchemaMismatch()); - } - } - - // unbind the database - var unbindCommand = new UnbindDatabaseCommand(this.Messaging, this.BackendHelper, msiDatabase, msiDatabaseFile, OutputType.Product, this.ExportBasePath, this.IntermediateFolder, false, false, skipSummaryInfo: true); - var output = unbindCommand.Execute(); - - // index all the rows to easily find modified rows - var rows = new Dictionary(); - foreach (var table in output.Tables) - { - foreach (var row in table.Rows) - { - rows.Add(String.Concat(table.Name, ':', row.GetPrimaryKey('\t', " ")), row); - } - } - - // process the _TransformView rows into transform rows - foreach (var row in transformViewTable.Rows) - { - var tableName = (string)row[0]; - var columnName = (string)row[1]; - var primaryKeys = (string)row[2]; - - var table = transform.EnsureTable(this.TableDefinitions[tableName]); - - if ("CREATE" == columnName) // added table - { - table.Operation = TableOperation.Add; - } - else if ("DELETE" == columnName) // deleted row - { - var deletedRow = this.CreateRow(table, primaryKeys, false); - deletedRow.Operation = RowOperation.Delete; - } - else if ("DROP" == columnName) // dropped table - { - table.Operation = TableOperation.Drop; - } - else if ("INSERT" == columnName) // added row - { - var index = String.Concat(tableName, ':', primaryKeys); - var addedRow = rows[index]; - addedRow.Operation = RowOperation.Add; - table.Rows.Add(addedRow); - } - else if (null != primaryKeys) // modified row - { - var index = String.Concat(tableName, ':', primaryKeys); - - // the _TransformView table includes information for added rows - // that looks like modified rows so it sometimes needs to be ignored - if (!addedRows.ContainsKey(index)) - { - var modifiedRow = rows[index]; - - // mark the field as modified - var indexOfModifiedValue = -1; - for (var i = 0; i < modifiedRow.TableDefinition.Columns.Length; ++i) - { - if (columnName.Equals(modifiedRow.TableDefinition.Columns[i].Name, StringComparison.Ordinal)) - { - indexOfModifiedValue = i; - break; - } - } - modifiedRow.Fields[indexOfModifiedValue].Modified = true; - - // move the modified row into the transform the first time its encountered - if (RowOperation.None == modifiedRow.Operation) - { - modifiedRow.Operation = RowOperation.Modify; - table.Rows.Add(modifiedRow); - } - } - } - else // added column - { - var column = table.Definition.Columns.Single(c => c.Name.Equals(columnName, StringComparison.Ordinal)); - column.Added = true; - } - } - } - - return transform; - } - - /// - /// Create a deleted or modified row. - /// - /// The table containing the row. - /// The primary keys of the row. - /// Option to set all required fields with placeholder values. - /// The new row. - private Row CreateRow(Table table, string primaryKeys, bool setRequiredFields) - { - var row = table.CreateRow(null); - - var primaryKeyParts = primaryKeys.Split('\t'); - var primaryKeyPartIndex = 0; - - for (var i = 0; i < table.Definition.Columns.Length; i++) - { - var columnDefinition = table.Definition.Columns[i]; - - if (columnDefinition.PrimaryKey) - { - if (ColumnType.Number == columnDefinition.Type && !columnDefinition.IsLocalizable) - { - row[i] = Convert.ToInt32(primaryKeyParts[primaryKeyPartIndex++], CultureInfo.InvariantCulture); - } - else - { - row[i] = primaryKeyParts[primaryKeyPartIndex++]; - } - } - else if (setRequiredFields) - { - if (ColumnType.Number == columnDefinition.Type && !columnDefinition.IsLocalizable) - { - row[i] = 1; - } - else if (ColumnType.Object == columnDefinition.Type) - { - if (null == this.EmptyFile) - { - this.EmptyFile = Path.Combine(this.IntermediateFolder, ".empty"); - using (var fileStream = File.Create(this.EmptyFile)) - { - } - } - - row[i] = this.EmptyFile; - } - else - { - row[i] = "1"; - } - } - } - - return row; - } - - private void GenerateDatabase(WindowsInstallerData output, string databaseFile) - { - var command = new GenerateDatabaseCommand(this.Messaging, null, null, output, databaseFile, this.TableDefinitions, this.IntermediateFolder, keepAddedColumns: true, suppressAddingValidationRows: true, useSubdirectory: false); - command.Execute(); - } - } -} diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindTransformCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindTransformCommand.cs new file mode 100644 index 00000000..ea40fa9f --- /dev/null +++ b/src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindTransformCommand.cs @@ -0,0 +1,309 @@ +// 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. + +namespace WixToolset.Core.WindowsInstaller.Unbind +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.ComponentModel; + using System.Globalization; + using System.IO; + using System.Linq; + using WixToolset.Core.Native.Msi; + using WixToolset.Core.WindowsInstaller.Bind; + using WixToolset.Data; + using WixToolset.Data.WindowsInstaller; + using WixToolset.Extensibility.Services; + + internal class UnbindTransformCommand + { + public UnbindTransformCommand(IMessaging messaging, IBackendHelper backendHelper, string transformFile, string exportBasePath, string intermediateFolder) + { + this.Messaging = messaging; + this.BackendHelper = backendHelper; + this.TransformFile = transformFile; + this.ExportBasePath = exportBasePath; + this.IntermediateFolder = intermediateFolder; + + this.TableDefinitions = new TableDefinitionCollection(WindowsInstallerTableDefinitions.All); + } + + private IMessaging Messaging { get; } + + private IBackendHelper BackendHelper { get; } + + private string TransformFile { get; } + + private string ExportBasePath { get; } + + private string IntermediateFolder { get; } + + private TableDefinitionCollection TableDefinitions { get; } + + private string EmptyFile { get; set; } + + public WindowsInstallerData Execute() + { + var transform = new WindowsInstallerData(new SourceLineNumber(this.TransformFile)); + transform.Type = OutputType.Transform; + + // get the summary information table + using (var summaryInformation = new SummaryInformation(this.TransformFile)) + { + var table = transform.EnsureTable(this.TableDefinitions["_SummaryInformation"]); + + for (var i = 1; 19 >= i; i++) + { + var value = summaryInformation.GetProperty(i); + + if (0 < value.Length) + { + var row = table.CreateRow(transform.SourceLineNumbers); + row[0] = i; + row[1] = value; + } + } + } + + // create a schema msi which hopefully matches the table schemas in the transform + var schemaOutput = new WindowsInstallerData(null); + var msiDatabaseFile = Path.Combine(this.IntermediateFolder, "schema.msi"); + foreach (var tableDefinition in this.TableDefinitions) + { + // skip unreal tables and the Patch table + if (!tableDefinition.Unreal && "Patch" != tableDefinition.Name) + { + schemaOutput.EnsureTable(tableDefinition); + } + } + + var addedRows = new Dictionary(); + Table transformViewTable; + + // Bind the schema msi. + this.GenerateDatabase(schemaOutput, msiDatabaseFile); + + // apply the transform to the database and retrieve the modifications + using (var msiDatabase = new Database(msiDatabaseFile, OpenDatabase.Transact)) + { + // apply the transform with the ViewTransform option to collect all the modifications + msiDatabase.ApplyTransform(this.TransformFile, TransformErrorConditions.All | TransformErrorConditions.ViewTransform); + + // unbind the database + var unbindCommand = new UnbindDatabaseCommand(this.Messaging, this.BackendHelper, msiDatabase, msiDatabaseFile, OutputType.Product, this.ExportBasePath, this.IntermediateFolder, false, false, skipSummaryInfo: true); + var transformViewOutput = unbindCommand.Execute(); + + // index the added and possibly modified rows (added rows may also appears as modified rows) + transformViewTable = transformViewOutput.Tables["_TransformView"]; + var modifiedRows = new Hashtable(); + foreach (var row in transformViewTable.Rows) + { + var tableName = (string)row[0]; + var columnName = (string)row[1]; + var primaryKeys = (string)row[2]; + + if ("INSERT" == columnName) + { + var index = String.Concat(tableName, ':', primaryKeys); + + addedRows.Add(index, null); + } + else if ("CREATE" != columnName && "DELETE" != columnName && "DROP" != columnName && null != primaryKeys) // modified row + { + var index = String.Concat(tableName, ':', primaryKeys); + + modifiedRows[index] = row; + } + } + + // create placeholder rows for modified rows to make the transform insert the updated values when its applied + foreach (Row row in modifiedRows.Values) + { + var tableName = (string)row[0]; + var columnName = (string)row[1]; + var primaryKeys = (string)row[2]; + + var index = String.Concat(tableName, ':', primaryKeys); + + // ignore information for added rows + if (!addedRows.ContainsKey(index)) + { + var table = schemaOutput.Tables[tableName]; + this.CreateRow(table, primaryKeys, true); + } + } + } + + // Re-bind the schema output with the placeholder rows. + this.GenerateDatabase(schemaOutput, msiDatabaseFile); + + // apply the transform to the database and retrieve the modifications + using (var msiDatabase = new Database(msiDatabaseFile, OpenDatabase.Transact)) + { + try + { + // apply the transform + msiDatabase.ApplyTransform(this.TransformFile, TransformErrorConditions.All); + + // commit the database to guard against weird errors with streams + msiDatabase.Commit(); + } + catch (Win32Exception ex) + { + if (0x65B == ex.NativeErrorCode) + { + // this commonly happens when the transform was built + // against a database schema different from the internal + // table definitions + throw new WixException(ErrorMessages.TransformSchemaMismatch()); + } + } + + // unbind the database + var unbindCommand = new UnbindDatabaseCommand(this.Messaging, this.BackendHelper, msiDatabase, msiDatabaseFile, OutputType.Product, this.ExportBasePath, this.IntermediateFolder, false, false, skipSummaryInfo: true); + var output = unbindCommand.Execute(); + + // index all the rows to easily find modified rows + var rows = new Dictionary(); + foreach (var table in output.Tables) + { + foreach (var row in table.Rows) + { + rows.Add(String.Concat(table.Name, ':', row.GetPrimaryKey('\t', " ")), row); + } + } + + // process the _TransformView rows into transform rows + foreach (var row in transformViewTable.Rows) + { + var tableName = (string)row[0]; + var columnName = (string)row[1]; + var primaryKeys = (string)row[2]; + + var table = transform.EnsureTable(this.TableDefinitions[tableName]); + + if ("CREATE" == columnName) // added table + { + table.Operation = TableOperation.Add; + } + else if ("DELETE" == columnName) // deleted row + { + var deletedRow = this.CreateRow(table, primaryKeys, false); + deletedRow.Operation = RowOperation.Delete; + } + else if ("DROP" == columnName) // dropped table + { + table.Operation = TableOperation.Drop; + } + else if ("INSERT" == columnName) // added row + { + var index = String.Concat(tableName, ':', primaryKeys); + var addedRow = rows[index]; + addedRow.Operation = RowOperation.Add; + table.Rows.Add(addedRow); + } + else if (null != primaryKeys) // modified row + { + var index = String.Concat(tableName, ':', primaryKeys); + + // the _TransformView table includes information for added rows + // that looks like modified rows so it sometimes needs to be ignored + if (!addedRows.ContainsKey(index)) + { + var modifiedRow = rows[index]; + + // mark the field as modified + var indexOfModifiedValue = -1; + for (var i = 0; i < modifiedRow.TableDefinition.Columns.Length; ++i) + { + if (columnName.Equals(modifiedRow.TableDefinition.Columns[i].Name, StringComparison.Ordinal)) + { + indexOfModifiedValue = i; + break; + } + } + modifiedRow.Fields[indexOfModifiedValue].Modified = true; + + // move the modified row into the transform the first time its encountered + if (RowOperation.None == modifiedRow.Operation) + { + modifiedRow.Operation = RowOperation.Modify; + table.Rows.Add(modifiedRow); + } + } + } + else // added column + { + var column = table.Definition.Columns.Single(c => c.Name.Equals(columnName, StringComparison.Ordinal)); + column.Added = true; + } + } + } + + return transform; + } + + /// + /// Create a deleted or modified row. + /// + /// The table containing the row. + /// The primary keys of the row. + /// Option to set all required fields with placeholder values. + /// The new row. + private Row CreateRow(Table table, string primaryKeys, bool setRequiredFields) + { + var row = table.CreateRow(null); + + var primaryKeyParts = primaryKeys.Split('\t'); + var primaryKeyPartIndex = 0; + + for (var i = 0; i < table.Definition.Columns.Length; i++) + { + var columnDefinition = table.Definition.Columns[i]; + + if (columnDefinition.PrimaryKey) + { + if (ColumnType.Number == columnDefinition.Type && !columnDefinition.IsLocalizable) + { + row[i] = Convert.ToInt32(primaryKeyParts[primaryKeyPartIndex++], CultureInfo.InvariantCulture); + } + else + { + row[i] = primaryKeyParts[primaryKeyPartIndex++]; + } + } + else if (setRequiredFields) + { + if (ColumnType.Number == columnDefinition.Type && !columnDefinition.IsLocalizable) + { + row[i] = 1; + } + else if (ColumnType.Object == columnDefinition.Type) + { + if (null == this.EmptyFile) + { + this.EmptyFile = Path.Combine(this.IntermediateFolder, ".empty"); + using (var fileStream = File.Create(this.EmptyFile)) + { + } + } + + row[i] = this.EmptyFile; + } + else + { + row[i] = "1"; + } + } + } + + return row; + } + + private void GenerateDatabase(WindowsInstallerData output, string databaseFile) + { + var command = new GenerateDatabaseCommand(this.Messaging, null, null, output, databaseFile, this.TableDefinitions, this.IntermediateFolder, keepAddedColumns: true, suppressAddingValidationRows: true, useSubdirectory: false); + command.Execute(); + } + } +} 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 @@ namespace WixToolset.Core.WindowsInstaller { + using System; using WixToolset.Data; internal static class WindowsInstallerBackendErrors { - //public static Message ReplaceThisWithTheFirstError(SourceLineNumber sourceLineNumbers) - //{ - // return Message(sourceLineNumbers, Ids.ReplaceThisWithTheFirstError, "format string", arg1, arg2); - //} + public static Message CannotLoadWixoutAsTransform(SourceLineNumber sourceLineNumbers, Exception exception) + { + var additionalDetail = exception == null ? String.Empty : ", detail: " + exception.Message; + + return Message(sourceLineNumbers, Ids.CannotLoadWixoutAsTransform, "Could not load wixout file as a transform{1}", additionalDetail); + } private static Message Message(SourceLineNumber sourceLineNumber, Ids id, string format, params object[] args) { @@ -18,7 +21,7 @@ namespace WixToolset.Core.WindowsInstaller public enum Ids { - // ReplaceThisWithTheFirstError = 7500, + CannotLoadWixoutAsTransform = 7500, } // last available is 7999. 8000 is BurnBackendErrors. } } 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 //case "patchcreation": //case ".pcp": // return new PatchCreationBackend(); - - case "transform": - case ".mst": - backend = new MstBackend(); - return true; } 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 @@ - A newer version of [ProductName] is already installed. - MsiPackage + A newer version en-us of [ProductName] is already installed. + MsiPackage en-us 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 @@ - A newer version of [ProductName] is already installed. - MsiPackage + A newer version ja-jp of [ProductName] is already installed. + MsiPackage ja-jp 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 @@ +// 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. + +namespace WixToolsetTest.CoreIntegration +{ + using System.IO; + using System.Linq; + using WixBuildTools.TestSupport; + using WixToolset.Core.TestPackage; + using WixToolset.Data.WindowsInstaller; + using Xunit; + + public class TransformFixture + { + [Fact] + public void CanBuildTransformFromEnuToJpn() + { + var folder = TestData.Get(@"TestData", "Language"); + + using (var fs = new DisposableFileSystem()) + { + var baseFolder = fs.GetFolder(); + var enuMsiPath = Path.Combine(baseFolder, @"bin\enu.msi"); + var jpnMsiPath = Path.Combine(baseFolder, @"bin\jpn.msi"); + var mstPath = Path.Combine(baseFolder, @"bin\test.mst"); + + var result = WixRunner.Execute(new[] + { + "build", + Path.Combine(folder, "Package.wxs"), + "-loc", Path.Combine(folder, "Package.en-us.wxl"), + "-bindpath", Path.Combine(folder, "data"), + "-intermediateFolder", Path.Combine(baseFolder, "obj"), + "-o", enuMsiPath + }); + result.AssertSuccess(); + + + result = WixRunner.Execute(new[] + { + "build", + Path.Combine(folder, "Package.wxs"), + "-loc", Path.Combine(folder, "Package.ja-jp.wxl"), + "-bindpath", Path.Combine(folder, "data"), + "-intermediateFolder", Path.Combine(baseFolder, "obj"), + "-o", jpnMsiPath + }); + result.AssertSuccess(); + + result = WixRunner.Execute(new[] + { + "msi", "transform", + "-intermediateFolder", Path.Combine(baseFolder, "obj"), + "-serr", "f", + "-o", mstPath, + enuMsiPath, + jpnMsiPath + }); + result.AssertSuccess(); + + Assert.True(File.Exists(mstPath)); + } + } + + [Fact] + public void CanBuildWixoutTransform() + { + var folder = TestData.Get(@"TestData", "Language"); + + using (var fs = new DisposableFileSystem()) + { + var baseFolder = fs.GetFolder(); + var enuMsiPath = Path.Combine(baseFolder, @"bin\enu.msi"); + var jpnMsiPath = Path.Combine(baseFolder, @"bin\jpn.msi"); + var wixmstPath = Path.Combine(baseFolder, @"bin\test.wixmst"); + var mstPath = Path.Combine(baseFolder, @"bin\test.mst"); + + var result = WixRunner.Execute(new[] + { + "build", + Path.Combine(folder, "Package.wxs"), + "-loc", Path.Combine(folder, "Package.en-us.wxl"), + "-bindpath", Path.Combine(folder, "data"), + "-intermediateFolder", Path.Combine(baseFolder, "obj"), + "-o", enuMsiPath + }); + result.AssertSuccess(); + + result = WixRunner.Execute(new[] + { + "build", + Path.Combine(folder, "Package.wxs"), + "-loc", Path.Combine(folder, "Package.ja-jp.wxl"), + "-bindpath", Path.Combine(folder, "data"), + "-intermediateFolder", Path.Combine(baseFolder, "obj"), + "-o", jpnMsiPath + }); + result.AssertSuccess(); + + result = WixRunner.Execute(new[] + { + "msi", "transform", + "-intermediateFolder", Path.Combine(baseFolder, "obj"), + "-serr", "f", + "-xo", + "-o", wixmstPath, + enuMsiPath, + jpnMsiPath + }); + result.AssertSuccess(); + + var wixmst = WindowsInstallerData.Load(wixmstPath); + var rows = wixmst.Tables.SelectMany(t => t.Rows).Where(r => r.Operation == RowOperation.Modify).ToDictionary(r => r.GetPrimaryKey()); + + WixAssert.CompareLineByLine(new[] + { + "NOT WIX_DOWNGRADE_DETECTED", + "ProductCode", + "ProductFeature", + "ProductLanguage" + }, rows.Keys.OrderBy(s => s).ToArray()); + + Assert.True(rows.TryGetValue("ProductFeature", out var productFeatureRow)); + Assert.Equal("MsiPackage ja-jp", productFeatureRow.FieldAsString(2)); + + Assert.True(rows.TryGetValue("ProductLanguage", out var productLanguageRow)); + Assert.Equal("1041", productLanguageRow.FieldAsString(1)); + + Assert.False(File.Exists(mstPath)); + + result = WixRunner.Execute(new[] + { + "msi", "transform", + "-intermediateFolder", Path.Combine(baseFolder, "obj"), + "-o", mstPath, + wixmstPath + }); + result.AssertSuccess(); + + Assert.True(File.Exists(mstPath)); + } + } + } +} -- cgit v1.2.3-55-g6feb