From dbde9e7104b907bbbaea17e21247d8cafc8b3a4c Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Sat, 14 Oct 2017 16:12:07 -0700 Subject: Massive refactoring to introduce the concept of IBackend --- .../Bind/AssignMediaCommand.cs | 314 +++++ .../Bind/BindDatabaseCommand.cs | 1282 ++++++++++++++++++++ .../Bind/BindSummaryInfoCommand.cs | 135 +++ .../Bind/BindTransformCommand.cs | 470 +++++++ .../Bind/CabinetBuilder.cs | 177 +++ .../Bind/CabinetResolver.cs | 122 ++ .../Bind/CabinetWorkItem.cs | 79 ++ .../Bind/ConfigurationCallback.cs | 91 ++ .../Bind/CopyTransformDataCommand.cs | 606 +++++++++ .../Bind/CreateCabinetsCommand.cs | 499 ++++++++ .../Bind/CreateDeltaPatchesCommand.cs | 87 ++ .../Bind/CreateSpecialPropertiesCommand.cs | 68 ++ .../Bind/ExtractMergeModuleFilesCommand.cs | 226 ++++ .../Bind/GenerateDatabaseCommand.cs | 332 +++++ .../Bind/GetFileFacadesCommand.cs | 149 +++ .../Bind/MergeModulesCommand.cs | 351 ++++++ .../Bind/ProcessUncompressedFilesCommand.cs | 118 ++ .../Bind/UpdateControlTextCommand.cs | 80 ++ .../Bind/UpdateFileFacadesCommand.cs | 533 ++++++++ 19 files changed, 5719 insertions(+) create mode 100644 src/WixToolset.Core.WindowsInstaller/Bind/AssignMediaCommand.cs create mode 100644 src/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs create mode 100644 src/WixToolset.Core.WindowsInstaller/Bind/BindSummaryInfoCommand.cs create mode 100644 src/WixToolset.Core.WindowsInstaller/Bind/BindTransformCommand.cs create mode 100644 src/WixToolset.Core.WindowsInstaller/Bind/CabinetBuilder.cs create mode 100644 src/WixToolset.Core.WindowsInstaller/Bind/CabinetResolver.cs create mode 100644 src/WixToolset.Core.WindowsInstaller/Bind/CabinetWorkItem.cs create mode 100644 src/WixToolset.Core.WindowsInstaller/Bind/ConfigurationCallback.cs create mode 100644 src/WixToolset.Core.WindowsInstaller/Bind/CopyTransformDataCommand.cs create mode 100644 src/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs create mode 100644 src/WixToolset.Core.WindowsInstaller/Bind/CreateDeltaPatchesCommand.cs create mode 100644 src/WixToolset.Core.WindowsInstaller/Bind/CreateSpecialPropertiesCommand.cs create mode 100644 src/WixToolset.Core.WindowsInstaller/Bind/ExtractMergeModuleFilesCommand.cs create mode 100644 src/WixToolset.Core.WindowsInstaller/Bind/GenerateDatabaseCommand.cs create mode 100644 src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesCommand.cs create mode 100644 src/WixToolset.Core.WindowsInstaller/Bind/MergeModulesCommand.cs create mode 100644 src/WixToolset.Core.WindowsInstaller/Bind/ProcessUncompressedFilesCommand.cs create mode 100644 src/WixToolset.Core.WindowsInstaller/Bind/UpdateControlTextCommand.cs create mode 100644 src/WixToolset.Core.WindowsInstaller/Bind/UpdateFileFacadesCommand.cs (limited to 'src/WixToolset.Core.WindowsInstaller/Bind') diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/AssignMediaCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/AssignMediaCommand.cs new file mode 100644 index 00000000..23c481b7 --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Bind/AssignMediaCommand.cs @@ -0,0 +1,314 @@ +// 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.Databases +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using WixToolset.Core.Bind; + using WixToolset.Data; + using WixToolset.Data.Rows; + + /// + /// AssignMediaCommand assigns files to cabs based on Media or MediaTemplate rows. + /// + public class AssignMediaCommand + { + public AssignMediaCommand() + { + this.CabinetNameTemplate = "Cab{0}.cab"; + } + + public Output Output { private get; set; } + + public bool FilesCompressed { private get; set; } + + public string CabinetNameTemplate { private get; set; } + + public IEnumerable FileFacades { private get; set; } + + public TableDefinitionCollection TableDefinitions { private get; set; } + + /// + /// Gets cabinets with their file rows. + /// + public Dictionary> FileFacadesByCabinetMedia { get; private set; } + + /// + /// Get media rows. + /// + public RowDictionary MediaRows { get; private set; } + + /// + /// Get uncompressed file rows. This will contain file rows of File elements that are marked with compression=no. + /// This contains all the files when Package element is marked with compression=no + /// + public IEnumerable UncompressedFileFacades { get; private set; } + + public void Execute() + { + Dictionary> filesByCabinetMedia = new Dictionary>(); + + RowDictionary mediaRows = new RowDictionary(); + + List uncompressedFiles = new List(); + + MediaRow mergeModuleMediaRow = null; + Table mediaTable = this.Output.Tables["Media"]; + Table mediaTemplateTable = this.Output.Tables["WixMediaTemplate"]; + + // If both tables are authored, it is an error. + if ((mediaTemplateTable != null && mediaTemplateTable.Rows.Count > 0) && (mediaTable != null && mediaTable.Rows.Count > 1)) + { + throw new WixException(WixErrors.MediaTableCollision(null)); + } + + // When building merge module, all the files go to "#MergeModule.CABinet". + if (OutputType.Module == this.Output.Type) + { + Table mergeModuleMediaTable = new Table(null, this.TableDefinitions["Media"]); + mergeModuleMediaRow = (MediaRow)mergeModuleMediaTable.CreateRow(null); + mergeModuleMediaRow.Cabinet = "#MergeModule.CABinet"; + + filesByCabinetMedia.Add(mergeModuleMediaRow, new List()); + } + + if (OutputType.Module == this.Output.Type || null == mediaTemplateTable) + { + this.ManuallyAssignFiles(mediaTable, mergeModuleMediaRow, this.FileFacades, filesByCabinetMedia, mediaRows, uncompressedFiles); + } + else + { + this.AutoAssignFiles(mediaTable, this.FileFacades, filesByCabinetMedia, mediaRows, uncompressedFiles); + } + + this.FileFacadesByCabinetMedia = new Dictionary>(); + + foreach (var mediaRowWithFiles in filesByCabinetMedia) + { + this.FileFacadesByCabinetMedia.Add(mediaRowWithFiles.Key, mediaRowWithFiles.Value); + } + + this.MediaRows = mediaRows; + + this.UncompressedFileFacades = uncompressedFiles; + } + + /// + /// Assign files to cabinets based on MediaTemplate authoring. + /// + /// FileRowCollection + private void AutoAssignFiles(Table mediaTable, IEnumerable fileFacades, Dictionary> filesByCabinetMedia, RowDictionary mediaRows, List uncompressedFiles) + { + const int MaxCabIndex = 999; + + ulong currentPreCabSize = 0; + ulong maxPreCabSizeInBytes; + int maxPreCabSizeInMB = 0; + int currentCabIndex = 0; + + MediaRow currentMediaRow = null; + + Table mediaTemplateTable = this.Output.Tables["WixMediaTemplate"]; + + // Auto assign files to cabinets based on maximum uncompressed media size + mediaTable.Rows.Clear(); + WixMediaTemplateRow mediaTemplateRow = (WixMediaTemplateRow)mediaTemplateTable.Rows[0]; + + if (!String.IsNullOrEmpty(mediaTemplateRow.CabinetTemplate)) + { + this.CabinetNameTemplate = mediaTemplateRow.CabinetTemplate; + } + + string mumsString = Environment.GetEnvironmentVariable("WIX_MUMS"); + + try + { + // Override authored mums value if environment variable is authored. + if (!String.IsNullOrEmpty(mumsString)) + { + maxPreCabSizeInMB = Int32.Parse(mumsString); + } + else + { + maxPreCabSizeInMB = mediaTemplateRow.MaximumUncompressedMediaSize; + } + + maxPreCabSizeInBytes = (ulong)maxPreCabSizeInMB * 1024 * 1024; + } + catch (FormatException) + { + throw new WixException(WixErrors.IllegalEnvironmentVariable("WIX_MUMS", mumsString)); + } + catch (OverflowException) + { + throw new WixException(WixErrors.MaximumUncompressedMediaSizeTooLarge(null, maxPreCabSizeInMB)); + } + + foreach (FileFacade facade in this.FileFacades) + { + // When building a product, if the current file is not to be compressed or if + // the package set not to be compressed, don't cab it. + if (OutputType.Product == this.Output.Type && + (YesNoType.No == facade.File.Compressed || + (YesNoType.NotSet == facade.File.Compressed && !this.FilesCompressed))) + { + uncompressedFiles.Add(facade); + continue; + } + + if (currentCabIndex == MaxCabIndex) + { + // Associate current file with last cab (irrespective of the size) and cab index is not incremented anymore. + List cabinetFiles = filesByCabinetMedia[currentMediaRow]; + facade.WixFile.DiskId = currentCabIndex; + cabinetFiles.Add(facade); + continue; + } + + // Update current cab size. + currentPreCabSize += (ulong)facade.File.FileSize; + + if (currentPreCabSize > maxPreCabSizeInBytes) + { + // Overflow due to current file + currentMediaRow = this.AddMediaRow(mediaTemplateRow, mediaTable, ++currentCabIndex); + mediaRows.Add(currentMediaRow); + filesByCabinetMedia.Add(currentMediaRow, new List()); + + List cabinetFileRows = filesByCabinetMedia[currentMediaRow]; + facade.WixFile.DiskId = currentCabIndex; + cabinetFileRows.Add(facade); + // Now files larger than MaxUncompressedMediaSize will be the only file in its cabinet so as to respect MaxUncompressedMediaSize + currentPreCabSize = (ulong)facade.File.FileSize; + } + else + { + // File fits in the current cab. + if (currentMediaRow == null) + { + // Create new cab and MediaRow + currentMediaRow = this.AddMediaRow(mediaTemplateRow, mediaTable, ++currentCabIndex); + mediaRows.Add(currentMediaRow); + filesByCabinetMedia.Add(currentMediaRow, new List()); + } + + // Associate current file with current cab. + List cabinetFiles = filesByCabinetMedia[currentMediaRow]; + facade.WixFile.DiskId = currentCabIndex; + cabinetFiles.Add(facade); + } + } + + // If there are uncompressed files and no MediaRow, create a default one. + if (uncompressedFiles.Count > 0 && mediaTable.Rows.Count == 0) + { + MediaRow defaultMediaRow = (MediaRow)mediaTable.CreateRow(null); + defaultMediaRow.DiskId = 1; + mediaRows.Add(defaultMediaRow); + } + } + + /// + /// Assign files to cabinets based on Media authoring. + /// + /// + /// + /// + private void ManuallyAssignFiles(Table mediaTable, MediaRow mergeModuleMediaRow, IEnumerable fileFacades, Dictionary> filesByCabinetMedia, RowDictionary mediaRows, List uncompressedFiles) + { + if (OutputType.Module != this.Output.Type) + { + if (null != mediaTable) + { + Dictionary cabinetMediaRows = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + foreach (MediaRow mediaRow in mediaTable.Rows) + { + // If the Media row has a cabinet, make sure it is unique across all Media rows. + if (!String.IsNullOrEmpty(mediaRow.Cabinet)) + { + MediaRow existingRow; + if (cabinetMediaRows.TryGetValue(mediaRow.Cabinet, out existingRow)) + { + Messaging.Instance.OnMessage(WixErrors.DuplicateCabinetName(mediaRow.SourceLineNumbers, mediaRow.Cabinet)); + Messaging.Instance.OnMessage(WixErrors.DuplicateCabinetName2(existingRow.SourceLineNumbers, existingRow.Cabinet)); + } + else + { + cabinetMediaRows.Add(mediaRow.Cabinet, mediaRow); + } + } + + mediaRows.Add(mediaRow); + } + } + + foreach (MediaRow mediaRow in mediaRows.Values) + { + if (null != mediaRow.Cabinet) + { + filesByCabinetMedia.Add(mediaRow, new List()); + } + } + } + + foreach (FileFacade facade in fileFacades) + { + if (OutputType.Module == this.Output.Type) + { + filesByCabinetMedia[mergeModuleMediaRow].Add(facade); + } + else + { + MediaRow mediaRow; + if (!mediaRows.TryGetValue(facade.WixFile.DiskId.ToString(CultureInfo.InvariantCulture), out mediaRow)) + { + Messaging.Instance.OnMessage(WixErrors.MissingMedia(facade.File.SourceLineNumbers, facade.WixFile.DiskId)); + continue; + } + + // When building a product, if the current file is not to be compressed or if + // the package set not to be compressed, don't cab it. + if (OutputType.Product == this.Output.Type && + (YesNoType.No == facade.File.Compressed || + (YesNoType.NotSet == facade.File.Compressed && !this.FilesCompressed))) + { + uncompressedFiles.Add(facade); + } + else // file is marked compressed. + { + List cabinetFiles; + if (filesByCabinetMedia.TryGetValue(mediaRow, out cabinetFiles)) + { + cabinetFiles.Add(facade); + } + else + { + Messaging.Instance.OnMessage(WixErrors.ExpectedMediaCabinet(facade.File.SourceLineNumbers, facade.File.File, facade.WixFile.DiskId)); + } + } + } + } + } + + /// + /// Adds a row to the media table with cab name template filled in. + /// + /// + /// + /// + private MediaRow AddMediaRow(WixMediaTemplateRow mediaTemplateRow, Table mediaTable, int cabIndex) + { + MediaRow currentMediaRow = (MediaRow)mediaTable.CreateRow(mediaTemplateRow.SourceLineNumbers); + currentMediaRow.DiskId = cabIndex; + currentMediaRow.Cabinet = String.Format(CultureInfo.InvariantCulture, this.CabinetNameTemplate, cabIndex); + + Table wixMediaTable = this.Output.EnsureTable(this.TableDefinitions["WixMedia"]); + WixMediaRow row = (WixMediaRow)wixMediaTable.CreateRow(mediaTemplateRow.SourceLineNumbers); + row.DiskId = cabIndex; + row.CompressionLevel = mediaTemplateRow.CompressionLevel; + + return currentMediaRow; + } + } +} diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs new file mode 100644 index 00000000..2e2c5417 --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Bind/BindDatabaseCommand.cs @@ -0,0 +1,1282 @@ +// 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.Bind +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Linq; + using WixToolset.Bind; + using WixToolset.Core.Bind; + using WixToolset.Core.WindowsInstaller.Databases; + using WixToolset.Data; + using WixToolset.Data.Bind; + using WixToolset.Data.Rows; + using WixToolset.Extensibility; + using WixToolset.Msi; + + /// + /// Binds a databse. + /// + internal class BindDatabaseCommand + { + // As outlined in RFC 4122, this is our namespace for generating name-based (version 3) UUIDs. + private static readonly Guid WixComponentGuidNamespace = new Guid("{3064E5C6-FB63-4FE9-AC49-E446A792EFA5}"); + + public BindDatabaseCommand(IBindContext context, Validator validator) + { + this.TableDefinitions = WindowsInstallerStandard.GetTableDefinitions(); + + this.BindPaths = context.BindPaths; + this.CabbingThreadCount = context.CabbingThreadCount; + this.CabCachePath = context.CabCachePath; + this.Codepage = context.Codepage; + this.DefaultCompressionLevel = context.DefaultCompressionLevel; + this.DelayedFields = context.DelayedFields; + this.ExpectedEmbeddedFiles = context.ExpectedEmbeddedFiles; + this.Extensions = context.Extensions; + this.Output = context.IntermediateRepresentation; + this.OutputPath = context.OutputPath; + this.PdbFile = context.OutputPdbPath; + this.IntermediateFolder = context.IntermediateFolder; + this.Validator = validator; + this.WixVariableResolver = context.WixVariableResolver; + + this.BackendExtensions = context.ExtensionManager.Create(); + } + + private IEnumerable BindPaths { get; } + + private int Codepage { get; } + + private int CabbingThreadCount { get; } + + private string CabCachePath { get; } + + private CompressionLevel DefaultCompressionLevel { get; } + + public IEnumerable DelayedFields { get; } + + public IEnumerable ExpectedEmbeddedFiles { get; } + + public bool DeltaBinaryPatch { get; set; } + + private IEnumerable BackendExtensions { get; } + + private IEnumerable Extensions { get; } + + private IEnumerable InspectorExtensions { get; } + + private string PdbFile { get; } + + private Output Output { get; } + + private string OutputPath { get; } + + private bool SuppressAddingValidationRows { get; } + + private bool SuppressLayout { get; } + + private TableDefinitionCollection TableDefinitions { get; } + + private string IntermediateFolder { get; } + + private Validator Validator { get; } + + private IBindVariableResolver WixVariableResolver { get; } + + public IEnumerable FileTransfers { get; private set; } + + public IEnumerable ContentFilePaths { get; private set; } + + public void Execute() + { + List fileTransfers = new List(); + + HashSet suppressedTableNames = new HashSet(); + + // If there are any fields to resolve later, create the cache to populate during bind. + IDictionary variableCache = null; + if (this.DelayedFields.Any()) + { + variableCache = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + } + + this.LocalizeUI(this.Output.Tables); + + // Process the summary information table before the other tables. + bool compressed; + bool longNames; + int installerVersion; + string modularizationGuid; + { + BindSummaryInfoCommand command = new BindSummaryInfoCommand(); + command.Output = this.Output; + command.Execute(); + + compressed = command.Compressed; + longNames = command.LongNames; + installerVersion = command.InstallerVersion; + modularizationGuid = command.ModularizationGuid; + } + + // Stop processing if an error previously occurred. + if (Messaging.Instance.EncounteredError) + { + return; + } + + // Modularize identifiers and add tables with real streams to the import tables. + if (OutputType.Module == this.Output.Type) + { + // Gather all the suppress modularization identifiers + HashSet suppressModularizationIdentifiers = null; + Table wixSuppressModularizationTable = this.Output.Tables["WixSuppressModularization"]; + if (null != wixSuppressModularizationTable) + { + suppressModularizationIdentifiers = new HashSet(wixSuppressModularizationTable.Rows.Select(row => (string)row[0])); + } + + foreach (Table table in this.Output.Tables) + { + table.Modularize(modularizationGuid, suppressModularizationIdentifiers); + } + } + + // This must occur after all variables and source paths have been resolved and after modularization. + List fileFacades; + { + GetFileFacadesCommand command = new GetFileFacadesCommand(); + command.FileTable = this.Output.Tables["File"]; + command.WixFileTable = this.Output.Tables["WixFile"]; + command.WixDeltaPatchFileTable = this.Output.Tables["WixDeltaPatchFile"]; + command.WixDeltaPatchSymbolPathsTable = this.Output.Tables["WixDeltaPatchSymbolPaths"]; + command.Execute(); + + fileFacades = command.FileFacades; + } + + ////if (OutputType.Patch == this.Output.Type) + ////{ + //// foreach (SubStorage substorage in this.Output.SubStorages) + //// { + //// Output transform = substorage.Data; + + //// ResolveFieldsCommand command = new ResolveFieldsCommand(); + //// command.Tables = transform.Tables; + //// command.FilesWithEmbeddedFiles = filesWithEmbeddedFiles; + //// command.FileManagerCore = this.FileManagerCore; + //// command.FileManagers = this.FileManagers; + //// command.SupportDelayedResolution = false; + //// command.TempFilesLocation = this.TempFilesLocation; + //// command.WixVariableResolver = this.WixVariableResolver; + //// command.Execute(); + + //// this.MergeUnrealTables(transform.Tables); + //// } + ////} + + { + CreateSpecialPropertiesCommand command = new CreateSpecialPropertiesCommand(); + command.PropertyTable = this.Output.Tables["Property"]; + command.WixPropertyTable = this.Output.Tables["WixProperty"]; + command.Execute(); + } + + if (Messaging.Instance.EncounteredError) + { + return; + } + + // Add binder variables for all properties. + Table propertyTable = this.Output.Tables["Property"]; + if (null != propertyTable) + { + foreach (PropertyRow propertyRow in propertyTable.Rows) + { + // Set the ProductCode if it is to be generated. + if (OutputType.Product == this.Output.Type && "ProductCode".Equals(propertyRow.Property, StringComparison.Ordinal) && "*".Equals(propertyRow.Value, StringComparison.Ordinal)) + { + propertyRow.Value = Common.GenerateGuid(); + + // Update the target ProductCode in any instance transforms. + foreach (SubStorage subStorage in this.Output.SubStorages) + { + Output subStorageOutput = subStorage.Data; + if (OutputType.Transform != subStorageOutput.Type) + { + continue; + } + + Table instanceSummaryInformationTable = subStorageOutput.Tables["_SummaryInformation"]; + foreach (Row row in instanceSummaryInformationTable.Rows) + { + if ((int)SummaryInformation.Transform.ProductCodes == row.FieldAsInteger(0)) + { + row[1] = row.FieldAsString(1).Replace("*", propertyRow.Value); + break; + } + } + } + } + + // Add the property name and value to the variableCache. + if (null != variableCache) + { + string key = String.Concat("property.", Common.Demodularize(this.Output.Type, modularizationGuid, propertyRow.Property)); + variableCache[key] = propertyRow.Value; + } + } + } + + // Extract files that come from cabinet files (this does not extract files from merge modules). + { + ExtractEmbeddedFilesCommand command = new ExtractEmbeddedFilesCommand(); + command.FilesWithEmbeddedFiles = this.ExpectedEmbeddedFiles; + command.Execute(); + } + + if (OutputType.Product == this.Output.Type) + { + // Retrieve files and their information from merge modules. + Table wixMergeTable = this.Output.Tables["WixMerge"]; + + if (null != wixMergeTable) + { + ExtractMergeModuleFilesCommand command = new ExtractMergeModuleFilesCommand(); + command.FileFacades = fileFacades; + command.FileTable = this.Output.Tables["File"]; + command.WixFileTable = this.Output.Tables["WixFile"]; + command.WixMergeTable = wixMergeTable; + command.OutputInstallerVersion = installerVersion; + command.SuppressLayout = this.SuppressLayout; + command.TempFilesLocation = this.IntermediateFolder; + command.Execute(); + + fileFacades.AddRange(command.MergeModulesFileFacades); + } + } + else if (OutputType.Patch == this.Output.Type) + { + // Merge transform data into the output object. + IEnumerable filesFromTransform = this.CopyFromTransformData(this.Output); + + fileFacades.AddRange(filesFromTransform); + } + + // stop processing if an error previously occurred + if (Messaging.Instance.EncounteredError) + { + return; + } + + Messaging.Instance.OnMessage(WixVerboses.UpdatingFileInformation()); + + // Gather information about files that did not come from merge modules (i.e. rows with a reference to the File table). + { + UpdateFileFacadesCommand command = new UpdateFileFacadesCommand(); + command.FileFacades = fileFacades; + command.UpdateFileFacades = fileFacades.Where(f => !f.FromModule); + command.ModularizationGuid = modularizationGuid; + command.Output = this.Output; + command.OverwriteHash = true; + command.TableDefinitions = this.TableDefinitions; + command.VariableCache = variableCache; + command.Execute(); + } + + // Set generated component guids. + this.SetComponentGuids(this.Output); + + // With the Component Guids set now we can create instance transforms. + this.CreateInstanceTransforms(this.Output); + + this.ValidateComponentGuids(this.Output); + + this.UpdateControlText(this.Output); + + if (this.DelayedFields.Any()) + { + ResolveDelayedFieldsCommand command = new ResolveDelayedFieldsCommand(); + command.OutputType = this.Output.Type; + command.DelayedFields = this.DelayedFields; + command.ModularizationGuid = null; + command.VariableCache = variableCache; + command.Execute(); + } + + // Assign files to media. + RowDictionary assignedMediaRows; + Dictionary> filesByCabinetMedia; + IEnumerable uncompressedFiles; + { + AssignMediaCommand command = new AssignMediaCommand(); + command.FilesCompressed = compressed; + command.FileFacades = fileFacades; + command.Output = this.Output; + command.TableDefinitions = this.TableDefinitions; + command.Execute(); + + assignedMediaRows = command.MediaRows; + filesByCabinetMedia = command.FileFacadesByCabinetMedia; + uncompressedFiles = command.UncompressedFileFacades; + } + + // Update file sequence. + this.UpdateMediaSequences(this.Output.Type, fileFacades, assignedMediaRows); + + // stop processing if an error previously occurred + if (Messaging.Instance.EncounteredError) + { + return; + } + + // Extended binder extensions can be called now that fields are resolved. + { + Table updatedFiles = this.Output.EnsureTable(this.TableDefinitions["WixBindUpdatedFiles"]); + + foreach (IBinderExtension extension in this.Extensions) + { + extension.AfterResolvedFields(this.Output); + } + + List updatedFileFacades = new List(); + + foreach (Row updatedFile in updatedFiles.Rows) + { + string updatedId = updatedFile.FieldAsString(0); + + FileFacade updatedFacade = fileFacades.First(f => f.File.File.Equals(updatedId)); + + updatedFileFacades.Add(updatedFacade); + } + + if (updatedFileFacades.Any()) + { + UpdateFileFacadesCommand command = new UpdateFileFacadesCommand(); + command.FileFacades = fileFacades; + command.UpdateFileFacades = updatedFileFacades; + command.ModularizationGuid = modularizationGuid; + command.Output = this.Output; + command.OverwriteHash = true; + command.TableDefinitions = this.TableDefinitions; + command.VariableCache = variableCache; + command.Execute(); + } + } + + // stop processing if an error previously occurred + if (Messaging.Instance.EncounteredError) + { + return; + } + + Directory.CreateDirectory(this.IntermediateFolder); + + if (OutputType.Patch == this.Output.Type && this.DeltaBinaryPatch) + { + CreateDeltaPatchesCommand command = new CreateDeltaPatchesCommand(); + command.FileFacades = fileFacades; + command.WixPatchIdTable = this.Output.Tables["WixPatchId"]; + command.TempFilesLocation = this.IntermediateFolder; + command.Execute(); + } + + // create cabinet files and process uncompressed files + string layoutDirectory = Path.GetDirectoryName(this.OutputPath); + if (!this.SuppressLayout || OutputType.Module == this.Output.Type) + { + Messaging.Instance.OnMessage(WixVerboses.CreatingCabinetFiles()); + + var command = new CreateCabinetsCommand(); + command.CabbingThreadCount = this.CabbingThreadCount; + command.CabCachePath = this.CabCachePath; + command.DefaultCompressionLevel = this.DefaultCompressionLevel; + command.Output = this.Output; + command.BackendExtensions = this.BackendExtensions; + command.LayoutDirectory = layoutDirectory; + command.Compressed = compressed; + command.FileRowsByCabinet = filesByCabinetMedia; + command.ResolveMedia = this.ResolveMedia; + command.TableDefinitions = this.TableDefinitions; + command.TempFilesLocation = this.IntermediateFolder; + command.WixMediaTable = this.Output.Tables["WixMedia"]; + command.Execute(); + + fileTransfers.AddRange(command.FileTransfers); + } + + if (OutputType.Patch == this.Output.Type) + { + // copy output data back into the transforms + this.CopyToTransformData(this.Output); + } + + // stop processing if an error previously occurred + if (Messaging.Instance.EncounteredError) + { + return; + } + + // add back suppressed tables which must be present prior to merging in modules + if (OutputType.Product == this.Output.Type) + { + Table wixMergeTable = this.Output.Tables["WixMerge"]; + + if (null != wixMergeTable && 0 < wixMergeTable.Rows.Count) + { + foreach (SequenceTable sequence in Enum.GetValues(typeof(SequenceTable))) + { + string sequenceTableName = sequence.ToString(); + Table sequenceTable = this.Output.Tables[sequenceTableName]; + + if (null == sequenceTable) + { + sequenceTable = this.Output.EnsureTable(this.TableDefinitions[sequenceTableName]); + } + + if (0 == sequenceTable.Rows.Count) + { + suppressedTableNames.Add(sequenceTableName); + } + } + } + } + + //foreach (BinderExtension extension in this.Extensions) + //{ + // extension.PostBind(this.Context); + //} + + // generate database file + Messaging.Instance.OnMessage(WixVerboses.GeneratingDatabase()); + string tempDatabaseFile = Path.Combine(this.IntermediateFolder, Path.GetFileName(this.OutputPath)); + this.GenerateDatabase(this.Output, tempDatabaseFile, false, false); + + FileTransfer transfer; + if (FileTransfer.TryCreate(tempDatabaseFile, this.OutputPath, true, this.Output.Type.ToString(), null, out transfer)) // note where this database needs to move in the future + { + transfer.Built = true; + fileTransfers.Add(transfer); + } + + // stop processing if an error previously occurred + if (Messaging.Instance.EncounteredError) + { + return; + } + + // Output the output to a file + Pdb pdb = new Pdb(); + pdb.Output = this.Output; + if (!String.IsNullOrEmpty(this.PdbFile)) + { + pdb.Save(this.PdbFile); + } + + // Merge modules. + if (OutputType.Product == this.Output.Type) + { + Messaging.Instance.OnMessage(WixVerboses.MergingModules()); + + MergeModulesCommand command = new MergeModulesCommand(); + command.FileFacades = fileFacades; + command.Output = this.Output; + command.OutputPath = tempDatabaseFile; + command.SuppressedTableNames = suppressedTableNames; + command.Execute(); + + // stop processing if an error previously occurred + if (Messaging.Instance.EncounteredError) + { + return; + } + } + + // inspect the MSI prior to running ICEs + //InspectorCore inspectorCore = new InspectorCore(); + //foreach (InspectorExtension inspectorExtension in this.InspectorExtensions) + //{ + // inspectorExtension.Core = inspectorCore; + // inspectorExtension.InspectDatabase(tempDatabaseFile, pdb); + + // inspectorExtension.Core = null; // reset. + //} + + if (Messaging.Instance.EncounteredError) + { + return; + } + + // validate the output if there is an MSI validator + if (null != this.Validator) + { + Stopwatch stopwatch = Stopwatch.StartNew(); + + // set the output file for source line information + this.Validator.Output = this.Output; + + Messaging.Instance.OnMessage(WixVerboses.ValidatingDatabase()); + + this.Validator.Validate(tempDatabaseFile); + + stopwatch.Stop(); + Messaging.Instance.OnMessage(WixVerboses.ValidatedDatabase(stopwatch.ElapsedMilliseconds)); + + // Stop processing if an error occurred. + if (Messaging.Instance.EncounteredError) + { + return; + } + } + + // Process uncompressed files. + if (!Messaging.Instance.EncounteredError && !this.SuppressLayout && uncompressedFiles.Any()) + { + var command = new ProcessUncompressedFilesCommand(); + command.Compressed = compressed; + command.FileFacades = uncompressedFiles; + command.LayoutDirectory = layoutDirectory; + command.LongNamesInImage = longNames; + command.MediaRows = assignedMediaRows; + command.ResolveMedia = this.ResolveMedia; + command.DatabasePath = tempDatabaseFile; + command.WixMediaTable = this.Output.Tables["WixMedia"]; + command.Execute(); + + fileTransfers.AddRange(command.FileTransfers); + } + + this.FileTransfers = fileTransfers; + this.ContentFilePaths = fileFacades.Select(r => r.WixFile.Source).ToList(); + } + + /// + /// Localize dialogs and controls. + /// + /// The tables to localize. + private void LocalizeUI(TableIndexedCollection tables) + { + Table dialogTable = tables["Dialog"]; + if (null != dialogTable) + { + foreach (Row row in dialogTable.Rows) + { + string dialog = (string)row[0]; + + if (this.WixVariableResolver.TryGetLocalizedControl(dialog, null, out LocalizedControl localizedControl)) + { + if (CompilerConstants.IntegerNotSet != localizedControl.X) + { + row[1] = localizedControl.X; + } + + if (CompilerConstants.IntegerNotSet != localizedControl.Y) + { + row[2] = localizedControl.Y; + } + + if (CompilerConstants.IntegerNotSet != localizedControl.Width) + { + row[3] = localizedControl.Width; + } + + if (CompilerConstants.IntegerNotSet != localizedControl.Height) + { + row[4] = localizedControl.Height; + } + + row[5] = (int)row[5] | localizedControl.Attributes; + + if (!String.IsNullOrEmpty(localizedControl.Text)) + { + row[6] = localizedControl.Text; + } + } + } + } + + Table controlTable = tables["Control"]; + if (null != controlTable) + { + foreach (Row row in controlTable.Rows) + { + string dialog = (string)row[0]; + string control = (string)row[1]; + + if (this.WixVariableResolver.TryGetLocalizedControl(dialog, control, out LocalizedControl localizedControl)) + { + if (CompilerConstants.IntegerNotSet != localizedControl.X) + { + row[3] = localizedControl.X.ToString(); + } + + if (CompilerConstants.IntegerNotSet != localizedControl.Y) + { + row[4] = localizedControl.Y.ToString(); + } + + if (CompilerConstants.IntegerNotSet != localizedControl.Width) + { + row[5] = localizedControl.Width.ToString(); + } + + if (CompilerConstants.IntegerNotSet != localizedControl.Height) + { + row[6] = localizedControl.Height.ToString(); + } + + row[7] = (int)row[7] | localizedControl.Attributes; + + if (!String.IsNullOrEmpty(localizedControl.Text)) + { + row[9] = localizedControl.Text; + } + } + } + } + } + + /// + /// Copy file data between transform substorages and the patch output object + /// + /// The output to bind. + /// True if copying from transform to patch, false the other way. + private IEnumerable CopyFromTransformData(Output output) + { + var command = new CopyTransformDataCommand(); + command.CopyOutFileRows = true; + command.Output = output; + command.TableDefinitions = this.TableDefinitions; + command.Execute(); + + return command.FileFacades; + } + + /// + /// Copy file data between transform substorages and the patch output object + /// + /// The output to bind. + /// True if copying from transform to patch, false the other way. + private void CopyToTransformData(Output output) + { + var command = new CopyTransformDataCommand(); + command.CopyOutFileRows = false; + command.Output = output; + command.TableDefinitions = this.TableDefinitions; + command.Execute(); + } + + private void UpdateMediaSequences(OutputType outputType, IEnumerable fileFacades, RowDictionary mediaRows) + { + // Calculate sequence numbers and media disk id layout for all file media information objects. + if (OutputType.Module == outputType) + { + int lastSequence = 0; + foreach (FileFacade facade in fileFacades) // TODO: Sort these rows directory path and component id and maybe file size or file extension and other creative ideas to get optimal install speed out of MSI. + { + facade.File.Sequence = ++lastSequence; + } + } + else + { + int lastSequence = 0; + MediaRow mediaRow = null; + Dictionary> patchGroups = new Dictionary>(); + + // sequence the non-patch-added files + foreach (FileFacade facade in fileFacades) // TODO: Sort these rows directory path and component id and maybe file size or file extension and other creative ideas to get optimal install speed out of MSI. + { + if (null == mediaRow) + { + mediaRow = mediaRows.Get(facade.WixFile.DiskId); + if (OutputType.Patch == outputType) + { + // patch Media cannot start at zero + lastSequence = mediaRow.LastSequence; + } + } + else if (mediaRow.DiskId != facade.WixFile.DiskId) + { + mediaRow.LastSequence = lastSequence; + mediaRow = mediaRows.Get(facade.WixFile.DiskId); + } + + if (0 < facade.WixFile.PatchGroup) + { + List patchGroup = patchGroups[facade.WixFile.PatchGroup]; + + if (null == patchGroup) + { + patchGroup = new List(); + patchGroups.Add(facade.WixFile.PatchGroup, patchGroup); + } + + patchGroup.Add(facade); + } + else + { + facade.File.Sequence = ++lastSequence; + } + } + + if (null != mediaRow) + { + mediaRow.LastSequence = lastSequence; + mediaRow = null; + } + + // sequence the patch-added files + foreach (List patchGroup in patchGroups.Values) + { + foreach (FileFacade facade in patchGroup) + { + if (null == mediaRow) + { + mediaRow = mediaRows.Get(facade.WixFile.DiskId); + } + else if (mediaRow.DiskId != facade.WixFile.DiskId) + { + mediaRow.LastSequence = lastSequence; + mediaRow = mediaRows.Get(facade.WixFile.DiskId); + } + + facade.File.Sequence = ++lastSequence; + } + } + + if (null != mediaRow) + { + mediaRow.LastSequence = lastSequence; + } + } + } + + /// + /// Set the guids for components with generatable guids. + /// + /// Internal representation of the database to operate on. + private void SetComponentGuids(Output output) + { + Table componentTable = output.Tables["Component"]; + if (null != componentTable) + { + Hashtable registryKeyRows = null; + Hashtable directories = null; + Hashtable componentIdGenSeeds = null; + Dictionary> fileRows = null; + + // find components with generatable guids + foreach (ComponentRow componentRow in componentTable.Rows) + { + // component guid will be generated + if ("*" == componentRow.Guid) + { + if (null == componentRow.KeyPath || componentRow.IsOdbcDataSourceKeyPath) + { + Messaging.Instance.OnMessage(WixErrors.IllegalComponentWithAutoGeneratedGuid(componentRow.SourceLineNumbers)); + } + else if (componentRow.IsRegistryKeyPath) + { + if (null == registryKeyRows) + { + Table registryTable = output.Tables["Registry"]; + + registryKeyRows = new Hashtable(registryTable.Rows.Count); + + foreach (Row registryRow in registryTable.Rows) + { + registryKeyRows.Add((string)registryRow[0], registryRow); + } + } + + Row foundRow = registryKeyRows[componentRow.KeyPath] as Row; + + string bitness = componentRow.Is64Bit ? "64" : String.Empty; + if (null != foundRow) + { + string regkey = String.Concat(bitness, foundRow[1], "\\", foundRow[2], "\\", foundRow[3]); + componentRow.Guid = Uuid.NewUuid(BindDatabaseCommand.WixComponentGuidNamespace, regkey.ToLowerInvariant()).ToString("B").ToUpperInvariant(); + } + } + else // must be a File KeyPath + { + // if the directory table hasn't been loaded into an indexed hash + // of directory ids to target names do that now. + if (null == directories) + { + Table directoryTable = output.Tables["Directory"]; + + int numDirectoryTableRows = (null != directoryTable) ? directoryTable.Rows.Count : 0; + + directories = new Hashtable(numDirectoryTableRows); + + // get the target paths for all directories + if (null != directoryTable) + { + foreach (Row row in directoryTable.Rows) + { + // if the directory Id already exists, we will skip it here since + // checking for duplicate primary keys is done later when importing tables + // into database + if (directories.ContainsKey(row[0])) + { + continue; + } + + string targetName = Common.GetName((string)row[2], false, true); + directories.Add(row[0], new ResolvedDirectory((string)row[1], targetName)); + } + } + } + + // if the component id generation seeds have not been indexed + // from the WixDirectory table do that now. + if (null == componentIdGenSeeds) + { + Table wixDirectoryTable = output.Tables["WixDirectory"]; + + int numWixDirectoryRows = (null != wixDirectoryTable) ? wixDirectoryTable.Rows.Count : 0; + + componentIdGenSeeds = new Hashtable(numWixDirectoryRows); + + // if there are any WixDirectory rows, build up the Component Guid + // generation seeds indexed by Directory/@Id. + if (null != wixDirectoryTable) + { + foreach (Row row in wixDirectoryTable.Rows) + { + componentIdGenSeeds.Add(row[0], (string)row[1]); + } + } + } + + // if the file rows have not been indexed by File.Component yet + // then do that now + if (null == fileRows) + { + Table fileTable = output.Tables["File"]; + + int numFileRows = (null != fileTable) ? fileTable.Rows.Count : 0; + + fileRows = new Dictionary>(numFileRows); + + if (null != fileTable) + { + foreach (FileRow file in fileTable.Rows) + { + List files; + if (!fileRows.TryGetValue(file.Component, out files)) + { + files = new List(); + fileRows.Add(file.Component, files); + } + + files.Add(file); + } + } + } + + // validate component meets all the conditions to have a generated guid + List currentComponentFiles = fileRows[componentRow.Component]; + int numFilesInComponent = currentComponentFiles.Count; + string path = null; + + foreach (FileRow fileRow in currentComponentFiles) + { + if (fileRow.File == componentRow.KeyPath) + { + // calculate the key file's canonical target path + string directoryPath = Binder.GetDirectoryPath(directories, componentIdGenSeeds, componentRow.Directory, true); + string fileName = Common.GetName(fileRow.FileName, false, true).ToLower(CultureInfo.InvariantCulture); + path = Path.Combine(directoryPath, fileName); + + // find paths that are not canonicalized + if (path.StartsWith(@"PersonalFolder\my pictures", StringComparison.Ordinal) || + path.StartsWith(@"ProgramFilesFolder\common files", StringComparison.Ordinal) || + path.StartsWith(@"ProgramMenuFolder\startup", StringComparison.Ordinal) || + path.StartsWith("TARGETDIR", StringComparison.Ordinal) || + path.StartsWith(@"StartMenuFolder\programs", StringComparison.Ordinal) || + path.StartsWith(@"WindowsFolder\fonts", StringComparison.Ordinal)) + { + Messaging.Instance.OnMessage(WixErrors.IllegalPathForGeneratedComponentGuid(componentRow.SourceLineNumbers, fileRow.Component, path)); + } + + // if component has more than one file, the key path must be versioned + if (1 < numFilesInComponent && String.IsNullOrEmpty(fileRow.Version)) + { + Messaging.Instance.OnMessage(WixErrors.IllegalGeneratedGuidComponentUnversionedKeypath(componentRow.SourceLineNumbers)); + } + } + else + { + // not a key path, so it must be an unversioned file if component has more than one file + if (1 < numFilesInComponent && !String.IsNullOrEmpty(fileRow.Version)) + { + Messaging.Instance.OnMessage(WixErrors.IllegalGeneratedGuidComponentVersionedNonkeypath(componentRow.SourceLineNumbers)); + } + } + } + + // if the rules were followed, reward with a generated guid + if (!Messaging.Instance.EncounteredError) + { + componentRow.Guid = Uuid.NewUuid(BindDatabaseCommand.WixComponentGuidNamespace, path).ToString("B").ToUpperInvariant(); + } + } + } + } + } + } + + /// + /// Creates instance transform substorages in the output. + /// + /// Output containing instance transform definitions. + private void CreateInstanceTransforms(Output output) + { + // Create and add substorages for instance transforms. + Table wixInstanceTransformsTable = output.Tables["WixInstanceTransforms"]; + if (null != wixInstanceTransformsTable && 0 <= wixInstanceTransformsTable.Rows.Count) + { + string targetProductCode = null; + string targetUpgradeCode = null; + string targetProductVersion = null; + + Table targetSummaryInformationTable = output.Tables["_SummaryInformation"]; + Table targetPropertyTable = output.Tables["Property"]; + + // Get the data from target database + foreach (Row propertyRow in targetPropertyTable.Rows) + { + if ("ProductCode" == (string)propertyRow[0]) + { + targetProductCode = (string)propertyRow[1]; + } + else if ("ProductVersion" == (string)propertyRow[0]) + { + targetProductVersion = (string)propertyRow[1]; + } + else if ("UpgradeCode" == (string)propertyRow[0]) + { + targetUpgradeCode = (string)propertyRow[1]; + } + } + + // Index the Instance Component Rows. + Dictionary instanceComponentGuids = new Dictionary(); + Table targetInstanceComponentTable = output.Tables["WixInstanceComponent"]; + if (null != targetInstanceComponentTable && 0 < targetInstanceComponentTable.Rows.Count) + { + foreach (Row row in targetInstanceComponentTable.Rows) + { + // Build up all the instances, we'll get the Components rows from the real Component table. + instanceComponentGuids.Add((string)row[0], null); + } + + Table targetComponentTable = output.Tables["Component"]; + foreach (ComponentRow componentRow in targetComponentTable.Rows) + { + string component = (string)componentRow[0]; + if (instanceComponentGuids.ContainsKey(component)) + { + instanceComponentGuids[component] = componentRow; + } + } + } + + // Generate the instance transforms + foreach (Row instanceRow in wixInstanceTransformsTable.Rows) + { + string instanceId = (string)instanceRow[0]; + + Output instanceTransform = new Output(instanceRow.SourceLineNumbers); + instanceTransform.Type = OutputType.Transform; + instanceTransform.Codepage = output.Codepage; + + Table instanceSummaryInformationTable = instanceTransform.EnsureTable(this.TableDefinitions["_SummaryInformation"]); + string targetPlatformAndLanguage = null; + + foreach (Row summaryInformationRow in targetSummaryInformationTable.Rows) + { + if (7 == (int)summaryInformationRow[0]) // PID_TEMPLATE + { + targetPlatformAndLanguage = (string)summaryInformationRow[1]; + } + + // Copy the row's data to the transform. + Row copyOfSummaryRow = instanceSummaryInformationTable.CreateRow(null); + copyOfSummaryRow[0] = summaryInformationRow[0]; + copyOfSummaryRow[1] = summaryInformationRow[1]; + } + + // Modify the appropriate properties. + Table propertyTable = instanceTransform.EnsureTable(this.TableDefinitions["Property"]); + + // Change the ProductCode property + string productCode = (string)instanceRow[2]; + if ("*" == productCode) + { + productCode = Common.GenerateGuid(); + } + + Row productCodeRow = propertyTable.CreateRow(instanceRow.SourceLineNumbers); + productCodeRow.Operation = RowOperation.Modify; + productCodeRow.Fields[1].Modified = true; + productCodeRow[0] = "ProductCode"; + productCodeRow[1] = productCode; + + // Change the instance property + Row instanceIdRow = propertyTable.CreateRow(instanceRow.SourceLineNumbers); + instanceIdRow.Operation = RowOperation.Modify; + instanceIdRow.Fields[1].Modified = true; + instanceIdRow[0] = (string)instanceRow[1]; + instanceIdRow[1] = instanceId; + + if (null != instanceRow[3]) + { + // Change the ProductName property + Row productNameRow = propertyTable.CreateRow(instanceRow.SourceLineNumbers); + productNameRow.Operation = RowOperation.Modify; + productNameRow.Fields[1].Modified = true; + productNameRow[0] = "ProductName"; + productNameRow[1] = (string)instanceRow[3]; + } + + if (null != instanceRow[4]) + { + // Change the UpgradeCode property + Row upgradeCodeRow = propertyTable.CreateRow(instanceRow.SourceLineNumbers); + upgradeCodeRow.Operation = RowOperation.Modify; + upgradeCodeRow.Fields[1].Modified = true; + upgradeCodeRow[0] = "UpgradeCode"; + upgradeCodeRow[1] = instanceRow[4]; + + // Change the Upgrade table + Table targetUpgradeTable = output.Tables["Upgrade"]; + if (null != targetUpgradeTable && 0 <= targetUpgradeTable.Rows.Count) + { + string upgradeId = (string)instanceRow[4]; + Table upgradeTable = instanceTransform.EnsureTable(this.TableDefinitions["Upgrade"]); + foreach (Row row in targetUpgradeTable.Rows) + { + // In case they are upgrading other codes to this new product, leave the ones that don't match the + // Product.UpgradeCode intact. + if (targetUpgradeCode == (string)row[0]) + { + Row upgradeRow = upgradeTable.CreateRow(null); + upgradeRow.Operation = RowOperation.Add; + upgradeRow.Fields[0].Modified = true; + // I was hoping to be able to RowOperation.Modify, but that didn't appear to function. + // upgradeRow.Fields[0].PreviousData = (string)row[0]; + + // Inserting a new Upgrade record with the updated UpgradeCode + upgradeRow[0] = upgradeId; + upgradeRow[1] = row[1]; + upgradeRow[2] = row[2]; + upgradeRow[3] = row[3]; + upgradeRow[4] = row[4]; + upgradeRow[5] = row[5]; + upgradeRow[6] = row[6]; + + // Delete the old row + Row upgradeRemoveRow = upgradeTable.CreateRow(null); + upgradeRemoveRow.Operation = RowOperation.Delete; + upgradeRemoveRow[0] = row[0]; + upgradeRemoveRow[1] = row[1]; + upgradeRemoveRow[2] = row[2]; + upgradeRemoveRow[3] = row[3]; + upgradeRemoveRow[4] = row[4]; + upgradeRemoveRow[5] = row[5]; + upgradeRemoveRow[6] = row[6]; + } + } + } + } + + // If there are instance Components generate new GUIDs for them. + if (0 < instanceComponentGuids.Count) + { + Table componentTable = instanceTransform.EnsureTable(this.TableDefinitions["Component"]); + foreach (ComponentRow targetComponentRow in instanceComponentGuids.Values) + { + string guid = targetComponentRow.Guid; + if (!String.IsNullOrEmpty(guid)) + { + Row instanceComponentRow = componentTable.CreateRow(targetComponentRow.SourceLineNumbers); + instanceComponentRow.Operation = RowOperation.Modify; + instanceComponentRow.Fields[1].Modified = true; + instanceComponentRow[0] = targetComponentRow[0]; + instanceComponentRow[1] = Uuid.NewUuid(BindDatabaseCommand.WixComponentGuidNamespace, String.Concat(guid, instanceId)).ToString("B").ToUpper(CultureInfo.InvariantCulture); + instanceComponentRow[2] = targetComponentRow[2]; + instanceComponentRow[3] = targetComponentRow[3]; + instanceComponentRow[4] = targetComponentRow[4]; + instanceComponentRow[5] = targetComponentRow[5]; + } + } + } + + // Update the summary information + Hashtable summaryRows = new Hashtable(instanceSummaryInformationTable.Rows.Count); + foreach (Row row in instanceSummaryInformationTable.Rows) + { + summaryRows[row[0]] = row; + + if ((int)SummaryInformation.Transform.UpdatedPlatformAndLanguage == (int)row[0]) + { + row[1] = targetPlatformAndLanguage; + } + else if ((int)SummaryInformation.Transform.ProductCodes == (int)row[0]) + { + row[1] = String.Concat(targetProductCode, targetProductVersion, ';', productCode, targetProductVersion, ';', targetUpgradeCode); + } + else if ((int)SummaryInformation.Transform.ValidationFlags == (int)row[0]) + { + row[1] = 0; + } + else if ((int)SummaryInformation.Transform.Security == (int)row[0]) + { + row[1] = "4"; + } + } + + if (!summaryRows.Contains((int)SummaryInformation.Transform.UpdatedPlatformAndLanguage)) + { + Row summaryRow = instanceSummaryInformationTable.CreateRow(null); + summaryRow[0] = (int)SummaryInformation.Transform.UpdatedPlatformAndLanguage; + summaryRow[1] = targetPlatformAndLanguage; + } + else if (!summaryRows.Contains((int)SummaryInformation.Transform.ValidationFlags)) + { + Row summaryRow = instanceSummaryInformationTable.CreateRow(null); + summaryRow[0] = (int)SummaryInformation.Transform.ValidationFlags; + summaryRow[1] = "0"; + } + else if (!summaryRows.Contains((int)SummaryInformation.Transform.Security)) + { + Row summaryRow = instanceSummaryInformationTable.CreateRow(null); + summaryRow[0] = (int)SummaryInformation.Transform.Security; + summaryRow[1] = "4"; + } + + output.SubStorages.Add(new SubStorage(instanceId, instanceTransform)); + } + } + } + + /// + /// Validate that there are no duplicate GUIDs in the output. + /// + /// + /// Duplicate GUIDs without conditions are an error condition; with conditions, it's a + /// warning, as the conditions might be mutually exclusive. + /// + private void ValidateComponentGuids(Output output) + { + Table componentTable = output.Tables["Component"]; + if (null != componentTable) + { + Dictionary componentGuidConditions = new Dictionary(componentTable.Rows.Count); + + foreach (ComponentRow row in componentTable.Rows) + { + // we don't care about unmanaged components and if there's a * GUID remaining, + // there's already an error that prevented it from being replaced with a real GUID. + if (!String.IsNullOrEmpty(row.Guid) && "*" != row.Guid) + { + bool thisComponentHasCondition = !String.IsNullOrEmpty(row.Condition); + bool allComponentsHaveConditions = thisComponentHasCondition; + + if (componentGuidConditions.ContainsKey(row.Guid)) + { + allComponentsHaveConditions = componentGuidConditions[row.Guid] && thisComponentHasCondition; + + if (allComponentsHaveConditions) + { + Messaging.Instance.OnMessage(WixWarnings.DuplicateComponentGuidsMustHaveMutuallyExclusiveConditions(row.SourceLineNumbers, row.Component, row.Guid)); + } + else + { + Messaging.Instance.OnMessage(WixErrors.DuplicateComponentGuids(row.SourceLineNumbers, row.Component, row.Guid)); + } + } + + componentGuidConditions[row.Guid] = allComponentsHaveConditions; + } + } + } + } + + /// + /// Update Control and BBControl text by reading from files when necessary. + /// + /// Internal representation of the msi database to operate upon. + private void UpdateControlText(Output output) + { + UpdateControlTextCommand command = new UpdateControlTextCommand(); + command.BBControlTable = output.Tables["BBControl"]; + command.WixBBControlTable = output.Tables["WixBBControl"]; + command.ControlTable = output.Tables["Control"]; + command.WixControlTable = output.Tables["WixControl"]; + command.Execute(); + } + + private string ResolveMedia(MediaRow mediaRow, string mediaLayoutDirectory, string layoutDirectory) + { + string layout = null; + + foreach (var extension in this.BackendExtensions) + { + layout = extension.ResolveMedia(mediaRow, mediaLayoutDirectory, layoutDirectory); + if (!String.IsNullOrEmpty(layout)) + { + break; + } + } + + // If no binder file manager resolved the layout, do the default behavior. + if (String.IsNullOrEmpty(layout)) + { + if (String.IsNullOrEmpty(mediaLayoutDirectory)) + { + layout = layoutDirectory; + } + else if (Path.IsPathRooted(mediaLayoutDirectory)) + { + layout = mediaLayoutDirectory; + } + else + { + layout = Path.Combine(layoutDirectory, mediaLayoutDirectory); + } + } + + return layout; + } + + /// + /// Creates the MSI/MSM/PCP database. + /// + /// Output to create database for. + /// The database file to create. + /// Whether to keep columns added in a transform. + /// Whether to use a subdirectory based on the file name for intermediate files. + private void GenerateDatabase(Output output, string databaseFile, bool keepAddedColumns, bool useSubdirectory) + { + var command = new GenerateDatabaseCommand(); + command.Extensions = this.Extensions; + command.Output = output; + command.OutputPath = databaseFile; + command.KeepAddedColumns = keepAddedColumns; + command.UseSubDirectory = useSubdirectory; + command.SuppressAddingValidationRows = this.SuppressAddingValidationRows; + command.TableDefinitions = this.TableDefinitions; + command.TempFilesLocation = this.IntermediateFolder; + command.Codepage = this.Codepage; + command.Execute(); + } + } +} diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/BindSummaryInfoCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/BindSummaryInfoCommand.cs new file mode 100644 index 00000000..5471792d --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Bind/BindSummaryInfoCommand.cs @@ -0,0 +1,135 @@ +// 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.Databases +{ + using System; + using System.Globalization; + using WixToolset.Data; + + /// + /// Binds the summary information table of a database. + /// + internal class BindSummaryInfoCommand + { + /// + /// The output to bind. + /// + public Output Output { private get; set; } + + /// + /// Returns a flag indicating if files are compressed by default. + /// + public bool Compressed { get; private set; } + + /// + /// Returns a flag indicating if uncompressed files use long filenames. + /// + public bool LongNames { get; private set; } + + public int InstallerVersion { get; private set; } + + /// + /// Modularization guid, or null if the output is not a module. + /// + public string ModularizationGuid { get; private set; } + + public void Execute() + { + this.Compressed = false; + this.LongNames = false; + this.InstallerVersion = 0; + this.ModularizationGuid = null; + + Table summaryInformationTable = this.Output.Tables["_SummaryInformation"]; + + if (null != summaryInformationTable) + { + bool foundCreateDataTime = false; + bool foundLastSaveDataTime = false; + bool foundCreatingApplication = false; + string now = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss", CultureInfo.InvariantCulture); + + foreach (Row summaryInformationRow in summaryInformationTable.Rows) + { + switch (summaryInformationRow.FieldAsInteger(0)) + { + case 1: // PID_CODEPAGE + // make sure the code page is an int and not a web name or null + string codepage = summaryInformationRow.FieldAsString(1); + + if (null == codepage) + { + codepage = "0"; + } + else + { + summaryInformationRow[1] = Common.GetValidCodePage(codepage, false, false, summaryInformationRow.SourceLineNumbers).ToString(CultureInfo.InvariantCulture); + } + break; + case 9: // PID_REVNUMBER + string packageCode = (string)summaryInformationRow[1]; + + if (OutputType.Module == this.Output.Type) + { + this.ModularizationGuid = packageCode.Substring(1, 36).Replace('-', '_'); + } + else if ("*" == packageCode) + { + // set the revision number (package/patch code) if it should be automatically generated + summaryInformationRow[1] = Common.GenerateGuid(); + } + break; + case 12: // PID_CREATE_DTM + foundCreateDataTime = true; + break; + case 13: // PID_LASTSAVE_DTM + foundLastSaveDataTime = true; + break; + case 14: + this.InstallerVersion = summaryInformationRow.FieldAsInteger(1); + break; + case 15: // PID_WORDCOUNT + if (OutputType.Patch == this.Output.Type) + { + this.LongNames = true; + this.Compressed = true; + } + else + { + this.LongNames = (0 == (summaryInformationRow.FieldAsInteger(1) & 1)); + this.Compressed = (2 == (summaryInformationRow.FieldAsInteger(1) & 2)); + } + break; + case 18: // PID_APPNAME + foundCreatingApplication = true; + break; + } + } + + // add a summary information row for the create time/date property if its not already set + if (!foundCreateDataTime) + { + Row createTimeDateRow = summaryInformationTable.CreateRow(null); + createTimeDateRow[0] = 12; + createTimeDateRow[1] = now; + } + + // add a summary information row for the last save time/date property if its not already set + if (!foundLastSaveDataTime) + { + Row lastSaveTimeDateRow = summaryInformationTable.CreateRow(null); + lastSaveTimeDateRow[0] = 13; + lastSaveTimeDateRow[1] = now; + } + + // add a summary information row for the creating application property if its not already set + if (!foundCreatingApplication) + { + Row creatingApplicationRow = summaryInformationTable.CreateRow(null); + creatingApplicationRow[0] = 18; + creatingApplicationRow[1] = String.Format(CultureInfo.InvariantCulture, AppCommon.GetCreatingApplicationString()); + } + } + } + } +} diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/BindTransformCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/BindTransformCommand.cs new file mode 100644 index 00000000..425d1f9c --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Bind/BindTransformCommand.cs @@ -0,0 +1,470 @@ +// 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.Databases +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using WixToolset.Data; + using WixToolset.Extensibility; + using WixToolset.Msi; + using WixToolset.Core.Native; + + internal class BindTransformCommand + { + public IEnumerable Extensions { private get; set; } + + public TableDefinitionCollection TableDefinitions { private get; set; } + + public string TempFilesLocation { private get; set; } + + public Output Transform { private get; set; } + + public string OutputPath { private get; set; } + + public void Execute() + { + int transformFlags = 0; + + Output targetOutput = new Output(null); + Output updatedOutput = new Output(null); + + // TODO: handle added columns + + // to generate a localized transform, both the target and updated + // databases need to have the same code page. the only reason to + // set different code pages is to support localized primary key + // columns, but that would only support deleting rows. if this + // becomes necessary, define a PreviousCodepage property on the + // Output class and persist this throughout transform generation. + targetOutput.Codepage = this.Transform.Codepage; + updatedOutput.Codepage = this.Transform.Codepage; + + // remove certain Property rows which will be populated from summary information values + string targetUpgradeCode = null; + string updatedUpgradeCode = null; + + Table propertyTable = this.Transform.Tables["Property"]; + if (null != propertyTable) + { + for (int i = propertyTable.Rows.Count - 1; i >= 0; i--) + { + Row row = propertyTable.Rows[i]; + + if ("ProductCode" == (string)row[0] || "ProductLanguage" == (string)row[0] || "ProductVersion" == (string)row[0] || "UpgradeCode" == (string)row[0]) + { + propertyTable.Rows.RemoveAt(i); + + if ("UpgradeCode" == (string)row[0]) + { + updatedUpgradeCode = (string)row[1]; + } + } + } + } + + Table targetSummaryInfo = targetOutput.EnsureTable(this.TableDefinitions["_SummaryInformation"]); + Table updatedSummaryInfo = updatedOutput.EnsureTable(this.TableDefinitions["_SummaryInformation"]); + Table targetPropertyTable = targetOutput.EnsureTable(this.TableDefinitions["Property"]); + Table updatedPropertyTable = updatedOutput.EnsureTable(this.TableDefinitions["Property"]); + + // process special summary information values + foreach (Row row in this.Transform.Tables["_SummaryInformation"].Rows) + { + if ((int)SummaryInformation.Transform.CodePage == (int)row[0]) + { + // convert from a web name if provided + string codePage = (string)row.Fields[1].Data; + if (null == codePage) + { + codePage = "0"; + } + else + { + codePage = Common.GetValidCodePage(codePage).ToString(CultureInfo.InvariantCulture); + } + + string previousCodePage = (string)row.Fields[1].PreviousData; + if (null == previousCodePage) + { + previousCodePage = "0"; + } + else + { + previousCodePage = Common.GetValidCodePage(previousCodePage).ToString(CultureInfo.InvariantCulture); + } + + Row targetCodePageRow = targetSummaryInfo.CreateRow(null); + targetCodePageRow[0] = 1; // PID_CODEPAGE + targetCodePageRow[1] = previousCodePage; + + Row updatedCodePageRow = updatedSummaryInfo.CreateRow(null); + updatedCodePageRow[0] = 1; // PID_CODEPAGE + updatedCodePageRow[1] = codePage; + } + else if ((int)SummaryInformation.Transform.TargetPlatformAndLanguage == (int)row[0] || + (int)SummaryInformation.Transform.UpdatedPlatformAndLanguage == (int)row[0]) + { + // the target language + string[] propertyData = ((string)row[1]).Split(';'); + string lang = 2 == propertyData.Length ? propertyData[1] : "0"; + + Table tempSummaryInfo = (int)SummaryInformation.Transform.TargetPlatformAndLanguage == (int)row[0] ? targetSummaryInfo : updatedSummaryInfo; + Table tempPropertyTable = (int)SummaryInformation.Transform.TargetPlatformAndLanguage == (int)row[0] ? targetPropertyTable : updatedPropertyTable; + + Row productLanguageRow = tempPropertyTable.CreateRow(null); + productLanguageRow[0] = "ProductLanguage"; + productLanguageRow[1] = lang; + + // set the platform;language on the MSI to be generated + Row templateRow = tempSummaryInfo.CreateRow(null); + templateRow[0] = 7; // PID_TEMPLATE + templateRow[1] = (string)row[1]; + } + else if ((int)SummaryInformation.Transform.ProductCodes == (int)row[0]) + { + string[] propertyData = ((string)row[1]).Split(';'); + + Row targetProductCodeRow = targetPropertyTable.CreateRow(null); + targetProductCodeRow[0] = "ProductCode"; + targetProductCodeRow[1] = propertyData[0].Substring(0, 38); + + Row targetProductVersionRow = targetPropertyTable.CreateRow(null); + targetProductVersionRow[0] = "ProductVersion"; + targetProductVersionRow[1] = propertyData[0].Substring(38); + + Row updatedProductCodeRow = updatedPropertyTable.CreateRow(null); + updatedProductCodeRow[0] = "ProductCode"; + updatedProductCodeRow[1] = propertyData[1].Substring(0, 38); + + Row updatedProductVersionRow = updatedPropertyTable.CreateRow(null); + updatedProductVersionRow[0] = "ProductVersion"; + updatedProductVersionRow[1] = propertyData[1].Substring(38); + + // UpgradeCode is optional and may not exists in the target + // or upgraded databases, so do not include a null-valued + // UpgradeCode property. + + targetUpgradeCode = propertyData[2]; + if (!String.IsNullOrEmpty(targetUpgradeCode)) + { + Row targetUpgradeCodeRow = targetPropertyTable.CreateRow(null); + targetUpgradeCodeRow[0] = "UpgradeCode"; + targetUpgradeCodeRow[1] = targetUpgradeCode; + + // If the target UpgradeCode is specified, an updated + // UpgradeCode is required. + if (String.IsNullOrEmpty(updatedUpgradeCode)) + { + updatedUpgradeCode = targetUpgradeCode; + } + } + + if (!String.IsNullOrEmpty(updatedUpgradeCode)) + { + Row updatedUpgradeCodeRow = updatedPropertyTable.CreateRow(null); + updatedUpgradeCodeRow[0] = "UpgradeCode"; + updatedUpgradeCodeRow[1] = updatedUpgradeCode; + } + } + else if ((int)SummaryInformation.Transform.ValidationFlags == (int)row[0]) + { + transformFlags = Convert.ToInt32(row[1], CultureInfo.InvariantCulture); + } + else if ((int)SummaryInformation.Transform.Reserved11 == (int)row[0]) + { + // PID_LASTPRINTED should be null for transforms + row.Operation = RowOperation.None; + } + else + { + // add everything else as is + Row targetRow = targetSummaryInfo.CreateRow(null); + targetRow[0] = row[0]; + targetRow[1] = row[1]; + + Row updatedRow = updatedSummaryInfo.CreateRow(null); + updatedRow[0] = row[0]; + updatedRow[1] = row[1]; + } + } + + // Validate that both databases have an UpgradeCode if the + // authoring transform will validate the UpgradeCode; otherwise, + // MsiCreateTransformSummaryinfo() will fail with 1620. + if (((int)TransformFlags.ValidateUpgradeCode & transformFlags) != 0 && + (String.IsNullOrEmpty(targetUpgradeCode) || String.IsNullOrEmpty(updatedUpgradeCode))) + { + Messaging.Instance.OnMessage(WixErrors.BothUpgradeCodesRequired()); + } + + string emptyFile = null; + + foreach (Table table in this.Transform.Tables) + { + // Ignore unreal tables when building transforms except the _Stream table. + // These tables are ignored when generating the database so there is no reason + // to process them here. + if (table.Definition.Unreal && "_Streams" != table.Name) + { + continue; + } + + // process table operations + switch (table.Operation) + { + case TableOperation.Add: + updatedOutput.EnsureTable(table.Definition); + break; + case TableOperation.Drop: + targetOutput.EnsureTable(table.Definition); + continue; + default: + targetOutput.EnsureTable(table.Definition); + updatedOutput.EnsureTable(table.Definition); + break; + } + + // process row operations + foreach (Row row in table.Rows) + { + switch (row.Operation) + { + case RowOperation.Add: + Table updatedTable = updatedOutput.EnsureTable(table.Definition); + updatedTable.Rows.Add(row); + continue; + case RowOperation.Delete: + Table targetTable = targetOutput.EnsureTable(table.Definition); + targetTable.Rows.Add(row); + + // fill-in non-primary key values + foreach (Field field in row.Fields) + { + if (!field.Column.PrimaryKey) + { + if (ColumnType.Number == field.Column.Type && !field.Column.IsLocalizable) + { + field.Data = field.Column.MinValue; + } + else if (ColumnType.Object == field.Column.Type) + { + if (null == emptyFile) + { + emptyFile = Path.Combine(this.TempFilesLocation, "empty"); + } + + field.Data = emptyFile; + } + else + { + field.Data = "0"; + } + } + } + continue; + } + + // Assure that the file table's sequence is populated + if ("File" == table.Name) + { + foreach (Row fileRow in table.Rows) + { + if (null == fileRow[7]) + { + if (RowOperation.Add == fileRow.Operation) + { + Messaging.Instance.OnMessage(WixErrors.InvalidAddedFileRowWithoutSequence(fileRow.SourceLineNumbers, (string)fileRow[0])); + break; + } + + // Set to 1 to prevent invalid IDT file from being generated + fileRow[7] = 1; + } + } + } + + // process modified and unmodified rows + bool modifiedRow = false; + Row targetRow = new Row(null, table.Definition); + Row updatedRow = row; + for (int i = 0; i < row.Fields.Length; i++) + { + Field updatedField = row.Fields[i]; + + if (updatedField.Modified) + { + // set a different value in the target row to ensure this value will be modified during transform generation + if (ColumnType.Number == updatedField.Column.Type && !updatedField.Column.IsLocalizable) + { + if (null == updatedField.Data || 1 != (int)updatedField.Data) + { + targetRow[i] = 1; + } + else + { + targetRow[i] = 2; + } + } + else if (ColumnType.Object == updatedField.Column.Type) + { + if (null == emptyFile) + { + emptyFile = Path.Combine(this.TempFilesLocation, "empty"); + } + + targetRow[i] = emptyFile; + } + else + { + if ("0" != (string)updatedField.Data) + { + targetRow[i] = "0"; + } + else + { + targetRow[i] = "1"; + } + } + + modifiedRow = true; + } + else if (ColumnType.Object == updatedField.Column.Type) + { + ObjectField objectField = (ObjectField)updatedField; + + // create an empty file for comparing against + if (null == objectField.PreviousData) + { + if (null == emptyFile) + { + emptyFile = Path.Combine(this.TempFilesLocation, "empty"); + } + + targetRow[i] = emptyFile; + modifiedRow = true; + } + else if (!this.CompareFiles(objectField.PreviousData, (string)objectField.Data)) + { + targetRow[i] = objectField.PreviousData; + modifiedRow = true; + } + } + else // unmodified + { + if (null != updatedField.Data) + { + targetRow[i] = updatedField.Data; + } + } + } + + // modified rows and certain special rows go in the target and updated msi databases + if (modifiedRow || + ("Property" == table.Name && + ("ProductCode" == (string)row[0] || + "ProductLanguage" == (string)row[0] || + "ProductVersion" == (string)row[0] || + "UpgradeCode" == (string)row[0]))) + { + Table targetTable = targetOutput.EnsureTable(table.Definition); + targetTable.Rows.Add(targetRow); + + Table updatedTable = updatedOutput.EnsureTable(table.Definition); + updatedTable.Rows.Add(updatedRow); + } + } + } + + //foreach (BinderExtension extension in this.Extensions) + //{ + // extension.PostBind(this.Context); + //} + + // Any errors encountered up to this point can cause errors during generation. + if (Messaging.Instance.EncounteredError) + { + return; + } + + string transformFileName = Path.GetFileNameWithoutExtension(this.OutputPath); + string targetDatabaseFile = Path.Combine(this.TempFilesLocation, String.Concat(transformFileName, "_target.msi")); + string updatedDatabaseFile = Path.Combine(this.TempFilesLocation, String.Concat(transformFileName, "_updated.msi")); + + try + { + if (!String.IsNullOrEmpty(emptyFile)) + { + using (FileStream fileStream = File.Create(emptyFile)) + { + } + } + + this.GenerateDatabase(targetOutput, targetDatabaseFile, false); + this.GenerateDatabase(updatedOutput, updatedDatabaseFile, true); + + // make sure the directory exists + Directory.CreateDirectory(Path.GetDirectoryName(this.OutputPath)); + + // create the transform file + using (Database targetDatabase = new Database(targetDatabaseFile, OpenDatabase.ReadOnly)) + { + using (Database updatedDatabase = new Database(updatedDatabaseFile, OpenDatabase.ReadOnly)) + { + if (updatedDatabase.GenerateTransform(targetDatabase, this.OutputPath)) + { + updatedDatabase.CreateTransformSummaryInfo(targetDatabase, this.OutputPath, (TransformErrorConditions)(transformFlags & 0xFFFF), (TransformValidations)((transformFlags >> 16) & 0xFFFF)); + } + else + { + Messaging.Instance.OnMessage(WixErrors.NoDifferencesInTransform(this.Transform.SourceLineNumbers)); + } + } + } + } + finally + { + if (!String.IsNullOrEmpty(emptyFile)) + { + File.Delete(emptyFile); + } + } + } + + private bool CompareFiles(string targetFile, string updatedFile) + { + bool? compared = null; + foreach (var extension in this.Extensions) + { + compared = extension.CompareFiles(targetFile, updatedFile); + if (compared.HasValue) + { + break; + } + } + + if (!compared.HasValue) + { + throw new InvalidOperationException(); // TODO: something needs to be said here that none of the binder file managers returned a result. + } + + return compared.Value; + } + + private void GenerateDatabase(Output output, string outputPath, bool keepAddedColumns) + { + var command = new GenerateDatabaseCommand(); + command.Codepage = output.Codepage; + command.Extensions = this.Extensions; + command.KeepAddedColumns = keepAddedColumns; + command.Output = output; + command.OutputPath = outputPath; + command.TableDefinitions = this.TableDefinitions; + command.TempFilesLocation = this.TempFilesLocation; + command.SuppressAddingValidationRows = true; + command.UseSubDirectory = true; + command.Execute(); + } + } +} diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/CabinetBuilder.cs b/src/WixToolset.Core.WindowsInstaller/Bind/CabinetBuilder.cs new file mode 100644 index 00000000..b2cc76fc --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Bind/CabinetBuilder.cs @@ -0,0 +1,177 @@ +// 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.Databases +{ + using System; + using System.Collections; + using System.IO; + using System.Linq; + using System.Threading; + using WixToolset.Core.Bind; + using WixToolset.Core.Cab; + using WixToolset.Data; + + /// + /// Builds cabinets using multiple threads. This implements a thread pool that generates cabinets with multiple + /// threads. Unlike System.Threading.ThreadPool, it waits until all threads are finished. + /// + internal sealed class CabinetBuilder + { + private Queue cabinetWorkItems; + private object lockObject; + private int threadCount; + + // Address of Binder's callback function for Cabinet Splitting + private IntPtr newCabNamesCallBackAddress; + + public int MaximumCabinetSizeForLargeFileSplitting { get; set; } + + public int MaximumUncompressedMediaSize { get; set; } + + /// + /// Instantiate a new CabinetBuilder. + /// + /// number of threads to use + /// Address of Binder's callback function for Cabinet Splitting + public CabinetBuilder(int threadCount, IntPtr newCabNamesCallBackAddress) + { + if (0 >= threadCount) + { + throw new ArgumentOutOfRangeException("threadCount"); + } + + this.cabinetWorkItems = new Queue(); + this.lockObject = new object(); + + this.threadCount = threadCount; + + // Set Address of Binder's callback function for Cabinet Splitting + this.newCabNamesCallBackAddress = newCabNamesCallBackAddress; + } + + /// + /// Enqueues a CabinetWorkItem to the queue. + /// + /// cabinet work item + public void Enqueue(CabinetWorkItem cabinetWorkItem) + { + this.cabinetWorkItems.Enqueue(cabinetWorkItem); + } + + /// + /// Create the queued cabinets. + /// + /// error message number (zero if no error) + public void CreateQueuedCabinets() + { + // don't create more threads than the number of cabinets to build + if (this.cabinetWorkItems.Count < this.threadCount) + { + this.threadCount = this.cabinetWorkItems.Count; + } + + if (0 < this.threadCount) + { + Thread[] threads = new Thread[this.threadCount]; + + for (int i = 0; i < threads.Length; i++) + { + threads[i] = new Thread(new ThreadStart(this.ProcessWorkItems)); + threads[i].Start(); + } + + // wait for all threads to finish + foreach (Thread thread in threads) + { + thread.Join(); + } + } + } + + /// + /// This function gets called by multiple threads to do actual work. + /// It takes one work item at a time and calls this.CreateCabinet(). + /// It does not return until cabinetWorkItems queue is empty + /// + private void ProcessWorkItems() + { + try + { + while (true) + { + CabinetWorkItem cabinetWorkItem; + + lock (this.cabinetWorkItems) + { + // check if there are any more cabinets to create + if (0 == this.cabinetWorkItems.Count) + { + break; + } + + cabinetWorkItem = (CabinetWorkItem)this.cabinetWorkItems.Dequeue(); + } + + // create a cabinet + this.CreateCabinet(cabinetWorkItem); + } + } + catch (WixException we) + { + Messaging.Instance.OnMessage(we.Error); + } + catch (Exception e) + { + Messaging.Instance.OnMessage(WixErrors.UnexpectedException(e.Message, e.GetType().ToString(), e.StackTrace)); + } + } + + /// + /// Creates a cabinet using the wixcab.dll interop layer. + /// + /// CabinetWorkItem containing information about the cabinet to create. + private void CreateCabinet(CabinetWorkItem cabinetWorkItem) + { + Messaging.Instance.OnMessage(WixVerboses.CreateCabinet(cabinetWorkItem.CabinetFile)); + + int maxCabinetSize = 0; // The value of 0 corresponds to default of 2GB which means no cabinet splitting + ulong maxPreCompressedSizeInBytes = 0; + + if (MaximumCabinetSizeForLargeFileSplitting != 0) + { + // User Specified Max Cab Size for File Splitting, So Check if this cabinet has a single file larger than MaximumUncompressedFileSize + // If a file is larger than MaximumUncompressedFileSize, then the cabinet containing it will have only this file + if (1 == cabinetWorkItem.FileFacades.Count()) + { + // Cabinet has Single File, Check if this is Large File than needs Splitting into Multiple cabs + // Get the Value for Max Uncompressed Media Size + maxPreCompressedSizeInBytes = (ulong)MaximumUncompressedMediaSize * 1024 * 1024; + + foreach (FileFacade facade in cabinetWorkItem.FileFacades) // No other easy way than looping to get the only row + { + if ((ulong)facade.File.FileSize >= maxPreCompressedSizeInBytes) + { + // If file is larger than MaximumUncompressedFileSize set Maximum Cabinet Size for Cabinet Splitting + maxCabinetSize = MaximumCabinetSizeForLargeFileSplitting; + } + } + } + } + + // create the cabinet file + string cabinetFileName = Path.GetFileName(cabinetWorkItem.CabinetFile); + string cabinetDirectory = Path.GetDirectoryName(cabinetWorkItem.CabinetFile); + + using (WixCreateCab cab = new WixCreateCab(cabinetFileName, cabinetDirectory, cabinetWorkItem.FileFacades.Count(), maxCabinetSize, cabinetWorkItem.MaxThreshold, cabinetWorkItem.CompressionLevel)) + { + foreach (FileFacade facade in cabinetWorkItem.FileFacades) + { + cab.AddFile(facade); + } + + cab.Complete(newCabNamesCallBackAddress); + } + } + } +} + diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/CabinetResolver.cs b/src/WixToolset.Core.WindowsInstaller/Bind/CabinetResolver.cs new file mode 100644 index 00000000..df1ccecf --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Bind/CabinetResolver.cs @@ -0,0 +1,122 @@ +// 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.Bind +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using WixToolset.Core.Cab; + using WixToolset.Core.Bind; + using WixToolset.Data; + using WixToolset.Extensibility; + + public class CabinetResolver + { + public CabinetResolver(string cabCachePath, IEnumerable backendExtensions) + { + this.CabCachePath = cabCachePath; + + this.BackendExtensions = backendExtensions; + } + + private string CabCachePath { get; } + + private IEnumerable BackendExtensions { get; } + + public ResolvedCabinet ResolveCabinet(string cabinetPath, IEnumerable fileFacades) + { + var filesWithPath = fileFacades.Select(f => new BindFileWithPath() { Id = f.File.File, Path = f.WixFile.Source }).ToList(); + + ResolvedCabinet resolved = null; + + foreach (var extension in this.BackendExtensions) + { + resolved = extension.ResolveCabinet(cabinetPath, filesWithPath); + + if (null != resolved) + { + return resolved; + } + } + + // By default cabinet should be built and moved to the suggested location. + resolved = new ResolvedCabinet() { BuildOption = CabinetBuildOption.BuildAndMove, Path = cabinetPath }; + + // If a cabinet cache path was provided, change the location for the cabinet + // to be built to and check if there is a cabinet that can be reused. + if (!String.IsNullOrEmpty(this.CabCachePath)) + { + string cabinetName = Path.GetFileName(cabinetPath); + resolved.Path = Path.Combine(this.CabCachePath, cabinetName); + + if (CheckFileExists(resolved.Path)) + { + // Assume that none of the following are true: + // 1. any files are added or removed + // 2. order of files changed or names changed + // 3. modified time changed + bool cabinetValid = true; + + // Need to force garbage collection of WixEnumerateCab to ensure the handle + // associated with it is closed before it is reused. + using (var wixEnumerateCab = new WixEnumerateCab()) + { + List fileList = wixEnumerateCab.Enumerate(resolved.Path); + + if (filesWithPath.Count() != fileList.Count) + { + cabinetValid = false; + } + else + { + int i = 0; + foreach (BindFileWithPath file in filesWithPath) + { + // First check that the file identifiers match because that is quick and easy. + CabinetFileInfo cabFileInfo = fileList[i]; + cabinetValid = (cabFileInfo.FileId == file.Id); + if (cabinetValid) + { + // Still valid so ensure the file sizes are the same. + FileInfo fileInfo = new FileInfo(file.Path); + cabinetValid = (cabFileInfo.Size == fileInfo.Length); + if (cabinetValid) + { + // Still valid so ensure the source time stamp hasn't changed. Thus we need + // to convert the source file time stamp into a cabinet compatible data/time. + Native.CabInterop.DateTimeToCabDateAndTime(fileInfo.LastWriteTime, out var sourceCabDate, out var sourceCabTime); + cabinetValid = (cabFileInfo.Date == sourceCabDate && cabFileInfo.Time == sourceCabTime); + } + } + + if (!cabinetValid) + { + break; + } + + i++; + } + } + } + + resolved.BuildOption = cabinetValid ? CabinetBuildOption.Copy : CabinetBuildOption.BuildAndCopy; + } + } + + return resolved; + } + + private static bool CheckFileExists(string path) + { + try + { + return File.Exists(path); + } + catch (ArgumentException) + { + throw new WixException(WixErrors.IllegalCharactersInPath(path)); + } + } + } +} diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/CabinetWorkItem.cs b/src/WixToolset.Core.WindowsInstaller/Bind/CabinetWorkItem.cs new file mode 100644 index 00000000..dcafcd36 --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Bind/CabinetWorkItem.cs @@ -0,0 +1,79 @@ +// 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.Databases +{ + using System.Collections.Generic; + using WixToolset.Core.Bind; + using WixToolset.Data; + using WixToolset.Data.Rows; + + /// + /// A cabinet builder work item. + /// + internal sealed class CabinetWorkItem + { + private string cabinetFile; + private CompressionLevel compressionLevel; + //private BinderFileManager binderFileManager; + private int maxThreshold; + + /// + /// Instantiate a new CabinetWorkItem. + /// + /// The collection of files in this cabinet. + /// The cabinet file. + /// Maximum threshold for each cabinet. + /// The compression level of the cabinet. + /// The binder file manager. + public CabinetWorkItem(IEnumerable fileFacades, string cabinetFile, int maxThreshold, CompressionLevel compressionLevel /*, BinderFileManager binderFileManager*/) + { + this.cabinetFile = cabinetFile; + this.compressionLevel = compressionLevel; + this.FileFacades = fileFacades; + //this.binderFileManager = binderFileManager; + this.maxThreshold = maxThreshold; + } + + /// + /// Gets the cabinet file. + /// + /// The cabinet file. + public string CabinetFile + { + get { return this.cabinetFile; } + } + + /// + /// Gets the compression level of the cabinet. + /// + /// The compression level of the cabinet. + public CompressionLevel CompressionLevel + { + get { return this.compressionLevel; } + } + + /// + /// Gets the collection of files in this cabinet. + /// + /// The collection of files in this cabinet. + public IEnumerable FileFacades { get; private set; } + + /// + /// Gets the binder file manager. + /// + /// The binder file manager. + //public BinderFileManager BinderFileManager + //{ + // get { return this.binderFileManager; } + //} + + /// + /// Gets the max threshold. + /// + /// The maximum threshold for a folder in a cabinet. + public int MaxThreshold + { + get { return this.maxThreshold; } + } + } +} diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/ConfigurationCallback.cs b/src/WixToolset.Core.WindowsInstaller/Bind/ConfigurationCallback.cs new file mode 100644 index 00000000..d4d3799f --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Bind/ConfigurationCallback.cs @@ -0,0 +1,91 @@ +// 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.Databases +{ + using System; + using System.Collections; + using System.Globalization; + using WixToolset.MergeMod; + + /// + /// Callback object for configurable merge modules. + /// + internal sealed class ConfigurationCallback : IMsmConfigureModule + { + private const int SOk = 0x0; + private const int SFalse = 0x1; + private Hashtable configurationData; + + /// + /// Creates a ConfigurationCallback object. + /// + /// String to break up into name/value pairs. + public ConfigurationCallback(string configData) + { + if (String.IsNullOrEmpty(configData)) + { + throw new ArgumentNullException("configData"); + } + + string[] pairs = configData.Split(','); + this.configurationData = new Hashtable(pairs.Length); + for (int i = 0; i < pairs.Length; ++i) + { + string[] nameVal = pairs[i].Split('='); + string name = nameVal[0]; + string value = nameVal[1]; + + name = name.Replace("%2C", ","); + name = name.Replace("%3D", "="); + name = name.Replace("%25", "%"); + + value = value.Replace("%2C", ","); + value = value.Replace("%3D", "="); + value = value.Replace("%25", "%"); + + this.configurationData[name] = value; + } + } + + /// + /// Returns text data based on name. + /// + /// Name of value to return. + /// Out param to put configuration data into. + /// S_OK if value provided, S_FALSE if not. + public int ProvideTextData(string name, out string configData) + { + if (this.configurationData.Contains(name)) + { + configData = (string)this.configurationData[name]; + return SOk; + } + else + { + configData = null; + return SFalse; + } + } + + /// + /// Returns integer data based on name. + /// + /// Name of value to return. + /// Out param to put configuration data into. + /// S_OK if value provided, S_FALSE if not. + public int ProvideIntegerData(string name, out int configData) + { + if (this.configurationData.Contains(name)) + { + string val = (string)this.configurationData[name]; + configData = Convert.ToInt32(val, CultureInfo.InvariantCulture); + return SOk; + } + else + { + configData = 0; + return SFalse; + } + } + } +} diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/CopyTransformDataCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/CopyTransformDataCommand.cs new file mode 100644 index 00000000..6388a352 --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Bind/CopyTransformDataCommand.cs @@ -0,0 +1,606 @@ +// 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.Databases +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using WixToolset.Data; + using WixToolset.Data.Rows; + using WixToolset.Extensibility; + using WixToolset.Core.Native; + using WixToolset.Core.Bind; + + internal class CopyTransformDataCommand + { + public bool CopyOutFileRows { private get; set; } + + public IEnumerable Extensions { private get; set; } + + public Output Output { private get; set; } + + public TableDefinitionCollection TableDefinitions { private get; set; } + + public IEnumerable FileFacades { get; private set; } + + public void Execute() + { + Debug.Assert(OutputType.Patch != this.Output.Type); + + List allFileRows = this.CopyOutFileRows ? new List() : null; + +#if false // TODO: Fix this patching related code to work correctly with FileFacades. + bool copyToPatch = (allFileRows != null); + bool copyFromPatch = !copyToPatch; + + RowDictionary patchMediaRows = new RowDictionary(); + + Dictionary> patchMediaFileRows = new Dictionary>(); + + Table patchActualFileTable = this.Output.EnsureTable(this.TableDefinitions["File"]); + Table patchFileTable = this.Output.EnsureTable(this.TableDefinitions["WixFile"]); + + if (copyFromPatch) + { + // index patch files by diskId+fileId + foreach (WixFileRow patchFileRow in patchFileTable.Rows) + { + int diskId = patchFileRow.DiskId; + RowDictionary mediaFileRows; + if (!patchMediaFileRows.TryGetValue(diskId, out mediaFileRows)) + { + mediaFileRows = new RowDictionary(); + patchMediaFileRows.Add(diskId, mediaFileRows); + } + + mediaFileRows.Add(patchFileRow); + } + + Table patchMediaTable = this.Output.EnsureTable(this.TableDefinitions["Media"]); + patchMediaRows = new RowDictionary(patchMediaTable); + } + + // index paired transforms + Dictionary pairedTransforms = new Dictionary(); + foreach (SubStorage substorage in this.Output.SubStorages) + { + if (substorage.Name.StartsWith("#")) + { + pairedTransforms.Add(substorage.Name.Substring(1), substorage.Data); + } + } + + try + { + // copy File bind data into substorages + foreach (SubStorage substorage in this.Output.SubStorages) + { + if (substorage.Name.StartsWith("#")) + { + // no changes necessary for paired transforms + continue; + } + + Output mainTransform = substorage.Data; + Table mainWixFileTable = mainTransform.Tables["WixFile"]; + Table mainMsiFileHashTable = mainTransform.Tables["MsiFileHash"]; + + this.FileManagerCore.ActiveSubStorage = substorage; + + RowDictionary mainWixFiles = new RowDictionary(mainWixFileTable); + RowDictionary mainMsiFileHashIndex = new RowDictionary(); + + Table mainFileTable = mainTransform.Tables["File"]; + Output pairedTransform = (Output)pairedTransforms[substorage.Name]; + + // copy Media.LastSequence and index the MsiFileHash table if it exists. + if (copyFromPatch) + { + Table pairedMediaTable = pairedTransform.Tables["Media"]; + foreach (MediaRow pairedMediaRow in pairedMediaTable.Rows) + { + MediaRow patchMediaRow = patchMediaRows.Get(pairedMediaRow.DiskId); + pairedMediaRow.Fields[1] = patchMediaRow.Fields[1]; + } + + if (null != mainMsiFileHashTable) + { + mainMsiFileHashIndex = new RowDictionary(mainMsiFileHashTable); + } + + // Validate file row changes for keypath-related issues + this.ValidateFileRowChanges(mainTransform); + } + + // Index File table of pairedTransform + Table pairedFileTable = pairedTransform.Tables["File"]; + RowDictionary pairedFileRows = new RowDictionary(pairedFileTable); + + if (null != mainFileTable) + { + if (copyFromPatch) + { + // Remove the MsiFileHash table because it will be updated later with the final file hash for each file + mainTransform.Tables.Remove("MsiFileHash"); + } + + foreach (FileRow mainFileRow in mainFileTable.Rows) + { + if (RowOperation.Delete == mainFileRow.Operation) + { + continue; + } + else if (RowOperation.None == mainFileRow.Operation && !copyToPatch) + { + continue; + } + + WixFileRow mainWixFileRow = mainWixFiles.Get(mainFileRow.File); + + if (copyToPatch) // when copying to the patch, we need compare the underlying files and include all file changes. + { + ObjectField objectField = (ObjectField)mainWixFileRow.Fields[6]; + FileRow pairedFileRow = pairedFileRows.Get(mainFileRow.File); + + // If the file is new, we always need to add it to the patch. + if (mainFileRow.Operation != RowOperation.Add) + { + // If PreviousData doesn't exist, target and upgrade layout point to the same location. No need to compare. + if (null == objectField.PreviousData) + { + if (mainFileRow.Operation == RowOperation.None) + { + continue; + } + } + else + { + // TODO: should this entire condition be placed in the binder file manager? + if ((0 == (PatchAttributeType.Ignore & mainWixFileRow.PatchAttributes)) && + !this.CompareFiles(objectField.PreviousData.ToString(), objectField.Data.ToString())) + { + // If the file is different, we need to mark the mainFileRow and pairedFileRow as modified. + mainFileRow.Operation = RowOperation.Modify; + if (null != pairedFileRow) + { + // Always patch-added, but never non-compressed. + pairedFileRow.Attributes |= MsiInterop.MsidbFileAttributesPatchAdded; + pairedFileRow.Attributes &= ~MsiInterop.MsidbFileAttributesNoncompressed; + pairedFileRow.Fields[6].Modified = true; + pairedFileRow.Operation = RowOperation.Modify; + } + } + else + { + // The File is same. We need mark all the attributes as unchanged. + mainFileRow.Operation = RowOperation.None; + foreach (Field field in mainFileRow.Fields) + { + field.Modified = false; + } + + if (null != pairedFileRow) + { + pairedFileRow.Attributes &= ~MsiInterop.MsidbFileAttributesPatchAdded; + pairedFileRow.Fields[6].Modified = false; + pairedFileRow.Operation = RowOperation.None; + } + continue; + } + } + } + else if (null != pairedFileRow) // RowOperation.Add + { + // Always patch-added, but never non-compressed. + pairedFileRow.Attributes |= MsiInterop.MsidbFileAttributesPatchAdded; + pairedFileRow.Attributes &= ~MsiInterop.MsidbFileAttributesNoncompressed; + pairedFileRow.Fields[6].Modified = true; + pairedFileRow.Operation = RowOperation.Add; + } + } + + // index patch files by diskId+fileId + int diskId = mainWixFileRow.DiskId; + + RowDictionary mediaFileRows; + if (!patchMediaFileRows.TryGetValue(diskId, out mediaFileRows)) + { + mediaFileRows = new RowDictionary(); + patchMediaFileRows.Add(diskId, mediaFileRows); + } + + string fileId = mainFileRow.File; + WixFileRow patchFileRow = mediaFileRows.Get(fileId); + if (copyToPatch) + { + if (null == patchFileRow) + { + FileRow patchActualFileRow = (FileRow)patchFileTable.CreateRow(mainFileRow.SourceLineNumbers); + patchActualFileRow.CopyFrom(mainFileRow); + + patchFileRow = (WixFileRow)patchFileTable.CreateRow(mainFileRow.SourceLineNumbers); + patchFileRow.CopyFrom(mainWixFileRow); + + mediaFileRows.Add(patchFileRow); + + allFileRows.Add(new FileFacade(patchActualFileRow, patchFileRow, null)); // TODO: should we be passing along delta information? Probably, right? + } + else + { + // TODO: confirm the rest of data is identical? + + // make sure Source is same. Otherwise we are silently ignoring a file. + if (0 != String.Compare(patchFileRow.Source, mainWixFileRow.Source, StringComparison.OrdinalIgnoreCase)) + { + Messaging.Instance.OnMessage(WixErrors.SameFileIdDifferentSource(mainFileRow.SourceLineNumbers, fileId, patchFileRow.Source, mainWixFileRow.Source)); + } + + // capture the previous file versions (and associated data) from this targeted instance of the baseline into the current filerow. + patchFileRow.AppendPreviousDataFrom(mainWixFileRow); + } + } + else + { + // copy data from the patch back to the transform + if (null != patchFileRow) + { + FileRow pairedFileRow = (FileRow)pairedFileRows.Get(fileId); + for (int i = 0; i < patchFileRow.Fields.Length; i++) + { + string patchValue = patchFileRow[i] == null ? "" : patchFileRow[i].ToString(); + string mainValue = mainFileRow[i] == null ? "" : mainFileRow[i].ToString(); + + if (1 == i) + { + // File.Component_ changes should not come from the shared file rows + // that contain the file information as each individual transform might + // have different changes (or no changes at all). + } + // File.Attributes should not changed for binary deltas + else if (6 == i) + { + if (null != patchFileRow.Patch) + { + // File.Attribute should not change for binary deltas + pairedFileRow.Attributes = mainFileRow.Attributes; + mainFileRow.Fields[i].Modified = false; + } + } + // File.Sequence is updated in pairedTransform, not mainTransform + else if (7 == i) + { + // file sequence is updated in Patch table instead of File table for delta patches + if (null != patchFileRow.Patch) + { + pairedFileRow.Fields[i].Modified = false; + } + else + { + pairedFileRow[i] = patchFileRow[i]; + pairedFileRow.Fields[i].Modified = true; + } + mainFileRow.Fields[i].Modified = false; + } + else if (patchValue != mainValue) + { + mainFileRow[i] = patchFileRow[i]; + mainFileRow.Fields[i].Modified = true; + if (mainFileRow.Operation == RowOperation.None) + { + mainFileRow.Operation = RowOperation.Modify; + } + } + } + + // copy MsiFileHash row for this File + Row patchHashRow; + if (!mainMsiFileHashIndex.TryGetValue(patchFileRow.File, out patchHashRow)) + { + patchHashRow = patchFileRow.Hash; + } + + if (null != patchHashRow) + { + Table mainHashTable = mainTransform.EnsureTable(this.TableDefinitions["MsiFileHash"]); + Row mainHashRow = mainHashTable.CreateRow(mainFileRow.SourceLineNumbers); + for (int i = 0; i < patchHashRow.Fields.Length; i++) + { + mainHashRow[i] = patchHashRow[i]; + if (i > 1) + { + // assume all hash fields have been modified + mainHashRow.Fields[i].Modified = true; + } + } + + // assume the MsiFileHash operation follows the File one + mainHashRow.Operation = mainFileRow.Operation; + } + + // copy MsiAssemblyName rows for this File + List patchAssemblyNameRows = patchFileRow.AssemblyNames; + if (null != patchAssemblyNameRows) + { + Table mainAssemblyNameTable = mainTransform.EnsureTable(this.TableDefinitions["MsiAssemblyName"]); + foreach (Row patchAssemblyNameRow in patchAssemblyNameRows) + { + // Copy if there isn't an identical modified/added row already in the transform. + bool foundMatchingModifiedRow = false; + foreach (Row mainAssemblyNameRow in mainAssemblyNameTable.Rows) + { + if (RowOperation.None != mainAssemblyNameRow.Operation && mainAssemblyNameRow.GetPrimaryKey('/').Equals(patchAssemblyNameRow.GetPrimaryKey('/'))) + { + foundMatchingModifiedRow = true; + break; + } + } + + if (!foundMatchingModifiedRow) + { + Row mainAssemblyNameRow = mainAssemblyNameTable.CreateRow(mainFileRow.SourceLineNumbers); + for (int i = 0; i < patchAssemblyNameRow.Fields.Length; i++) + { + mainAssemblyNameRow[i] = patchAssemblyNameRow[i]; + } + + // assume value field has been modified + mainAssemblyNameRow.Fields[2].Modified = true; + mainAssemblyNameRow.Operation = mainFileRow.Operation; + } + } + } + + // Add patch header for this file + if (null != patchFileRow.Patch) + { + // Add the PatchFiles action automatically to the AdminExecuteSequence and InstallExecuteSequence tables. + AddPatchFilesActionToSequenceTable(SequenceTable.AdminExecuteSequence, mainTransform, pairedTransform, mainFileRow); + AddPatchFilesActionToSequenceTable(SequenceTable.InstallExecuteSequence, mainTransform, pairedTransform, mainFileRow); + + // Add to Patch table + Table patchTable = pairedTransform.EnsureTable(this.TableDefinitions["Patch"]); + if (0 == patchTable.Rows.Count) + { + patchTable.Operation = TableOperation.Add; + } + + Row patchRow = patchTable.CreateRow(mainFileRow.SourceLineNumbers); + patchRow[0] = patchFileRow.File; + patchRow[1] = patchFileRow.Sequence; + + FileInfo patchFile = new FileInfo(patchFileRow.Source); + patchRow[2] = (int)patchFile.Length; + patchRow[3] = 0 == (PatchAttributeType.AllowIgnoreOnError & patchFileRow.PatchAttributes) ? 0 : 1; + + string streamName = patchTable.Name + "." + patchRow[0] + "." + patchRow[1]; + if (MsiInterop.MsiMaxStreamNameLength < streamName.Length) + { + streamName = "_" + Guid.NewGuid().ToString("D").ToUpperInvariant().Replace('-', '_'); + Table patchHeadersTable = pairedTransform.EnsureTable(this.TableDefinitions["MsiPatchHeaders"]); + if (0 == patchHeadersTable.Rows.Count) + { + patchHeadersTable.Operation = TableOperation.Add; + } + Row patchHeadersRow = patchHeadersTable.CreateRow(mainFileRow.SourceLineNumbers); + patchHeadersRow[0] = streamName; + patchHeadersRow[1] = patchFileRow.Patch; + patchRow[5] = streamName; + patchHeadersRow.Operation = RowOperation.Add; + } + else + { + patchRow[4] = patchFileRow.Patch; + } + patchRow.Operation = RowOperation.Add; + } + } + else + { + // TODO: throw because all transform rows should have made it into the patch + } + } + } + } + + if (copyFromPatch) + { + this.Output.Tables.Remove("Media"); + this.Output.Tables.Remove("File"); + this.Output.Tables.Remove("MsiFileHash"); + this.Output.Tables.Remove("MsiAssemblyName"); + } + } + } + finally + { + this.FileManagerCore.ActiveSubStorage = null; + } +#endif + this.FileFacades = allFileRows; + } + + /// + /// Adds the PatchFiles action to the sequence table if it does not already exist. + /// + /// The sequence table to check or modify. + /// The primary authoring transform. + /// The secondary patch transform. + /// The file row that contains information about the patched file. + private void AddPatchFilesActionToSequenceTable(SequenceTable table, Output mainTransform, Output pairedTransform, Row mainFileRow) + { + // Find/add PatchFiles action (also determine sequence for it). + // Search mainTransform first, then pairedTransform (pairedTransform overrides). + bool hasPatchFilesAction = false; + int seqInstallFiles = 0; + int seqDuplicateFiles = 0; + string tableName = table.ToString(); + + TestSequenceTableForPatchFilesAction( + mainTransform.Tables[tableName], + ref hasPatchFilesAction, + ref seqInstallFiles, + ref seqDuplicateFiles); + TestSequenceTableForPatchFilesAction( + pairedTransform.Tables[tableName], + ref hasPatchFilesAction, + ref seqInstallFiles, + ref seqDuplicateFiles); + if (!hasPatchFilesAction) + { + Table iesTable = pairedTransform.EnsureTable(this.TableDefinitions[tableName]); + if (0 == iesTable.Rows.Count) + { + iesTable.Operation = TableOperation.Add; + } + + Row patchAction = iesTable.CreateRow(null); + WixActionRow wixPatchAction = WindowsInstallerStandard.GetStandardActions()[table, "PatchFiles"]; + int sequence = wixPatchAction.Sequence; + // Test for default sequence value's appropriateness + if (seqInstallFiles >= sequence || (0 != seqDuplicateFiles && seqDuplicateFiles <= sequence)) + { + if (0 != seqDuplicateFiles) + { + if (seqDuplicateFiles < seqInstallFiles) + { + throw new WixException(WixErrors.InsertInvalidSequenceActionOrder(mainFileRow.SourceLineNumbers, iesTable.Name, "InstallFiles", "DuplicateFiles", wixPatchAction.Action)); + } + else + { + sequence = (seqDuplicateFiles + seqInstallFiles) / 2; + if (seqInstallFiles == sequence || seqDuplicateFiles == sequence) + { + throw new WixException(WixErrors.InsertSequenceNoSpace(mainFileRow.SourceLineNumbers, iesTable.Name, "InstallFiles", "DuplicateFiles", wixPatchAction.Action)); + } + } + } + else + { + sequence = seqInstallFiles + 1; + } + } + patchAction[0] = wixPatchAction.Action; + patchAction[1] = wixPatchAction.Condition; + patchAction[2] = sequence; + patchAction.Operation = RowOperation.Add; + } + } + + /// + /// Tests sequence table for PatchFiles and associated actions + /// + /// The table to test. + /// Set to true if PatchFiles action is found. Left unchanged otherwise. + /// Set to sequence value of InstallFiles action if found. Left unchanged otherwise. + /// Set to sequence value of DuplicateFiles action if found. Left unchanged otherwise. + private static void TestSequenceTableForPatchFilesAction(Table iesTable, ref bool hasPatchFilesAction, ref int seqInstallFiles, ref int seqDuplicateFiles) + { + if (null != iesTable) + { + foreach (Row iesRow in iesTable.Rows) + { + if (String.Equals("PatchFiles", (string)iesRow[0], StringComparison.Ordinal)) + { + hasPatchFilesAction = true; + } + if (String.Equals("InstallFiles", (string)iesRow[0], StringComparison.Ordinal)) + { + seqInstallFiles = (int)iesRow.Fields[2].Data; + } + if (String.Equals("DuplicateFiles", (string)iesRow[0], StringComparison.Ordinal)) + { + seqDuplicateFiles = (int)iesRow.Fields[2].Data; + } + } + } + } + + /// + /// Signal a warning if a non-keypath file was changed in a patch without also changing the keypath file of the component. + /// + /// The output to validate. + private void ValidateFileRowChanges(Output transform) + { + Table componentTable = transform.Tables["Component"]; + Table fileTable = transform.Tables["File"]; + + // There's no sense validating keypaths if the transform has no component or file table + if (componentTable == null || fileTable == null) + { + return; + } + + Dictionary componentKeyPath = new Dictionary(componentTable.Rows.Count); + + // Index the Component table for non-directory & non-registry key paths. + foreach (Row row in componentTable.Rows) + { + if (null != row.Fields[5].Data && + 0 != ((int)row.Fields[3].Data & MsiInterop.MsidbComponentAttributesRegistryKeyPath)) + { + componentKeyPath.Add(row.Fields[0].Data.ToString(), row.Fields[5].Data.ToString()); + } + } + + Dictionary componentWithChangedKeyPath = new Dictionary(); + Dictionary componentWithNonKeyPathChanged = new Dictionary(); + // Verify changes in the file table, now that file diffing has occurred + foreach (FileRow row in fileTable.Rows) + { + string fileId = row.Fields[0].Data.ToString(); + string componentId = row.Fields[1].Data.ToString(); + + if (RowOperation.Modify != row.Operation) + { + continue; + } + + // If this file is the keypath of a component + if (componentKeyPath.ContainsValue(fileId)) + { + if (!componentWithChangedKeyPath.ContainsKey(componentId)) + { + componentWithChangedKeyPath.Add(componentId, fileId); + } + } + else + { + if (!componentWithNonKeyPathChanged.ContainsKey(componentId)) + { + componentWithNonKeyPathChanged.Add(componentId, fileId); + } + } + } + + foreach (KeyValuePair componentFile in componentWithNonKeyPathChanged) + { + // Make sure all changes to non keypath files also had a change in the keypath. + if (!componentWithChangedKeyPath.ContainsKey(componentFile.Key) && componentKeyPath.ContainsKey(componentFile.Key)) + { + Messaging.Instance.OnMessage(WixWarnings.UpdateOfNonKeyPathFile((string)componentFile.Value, (string)componentFile.Key, (string)componentKeyPath[componentFile.Key])); + } + } + } + + private bool CompareFiles(string targetFile, string updatedFile) + { + bool? compared = null; + foreach (var extension in this.Extensions) + { + compared = extension.CompareFiles(targetFile, updatedFile); + + if (compared.HasValue) + { + break; + } + } + + if (!compared.HasValue) + { + throw new InvalidOperationException(); // TODO: something needs to be said here that none of the binder file managers returned a result. + } + + return compared.Value; + } + } +} diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs new file mode 100644 index 00000000..02015744 --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs @@ -0,0 +1,499 @@ +// 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.Databases +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Runtime.InteropServices; + using System.Threading; + using WixToolset.Core.Bind; + using WixToolset.Core.WindowsInstaller.Bind; + using WixToolset.Data; + using WixToolset.Data.Bind; + using WixToolset.Data.Rows; + using WixToolset.Extensibility; + + /// + /// Creates cabinet files. + /// + internal class CreateCabinetsCommand + { + public const int DefaultMaximumUncompressedMediaSize = 200; // Default value is 200 MB + public const int MaxValueOfMaxCabSizeForLargeFileSplitting = 2 * 1024; // 2048 MB (i.e. 2 GB) + + private List fileTransfers; + + private FileSplitCabNamesCallback newCabNamesCallBack; + + private Dictionary lastCabinetAddedToMediaTable; // Key is First Cabinet Name, Value is Last Cabinet Added in the Split Sequence + + public CreateCabinetsCommand() + { + this.fileTransfers = new List(); + + this.newCabNamesCallBack = this.NewCabNamesCallBack; + } + + /// + /// Sets the number of threads to use for cabinet creation. + /// + public int CabbingThreadCount { private get; set; } + + public string CabCachePath { private get; set; } + + public string TempFilesLocation { private get; set; } + + /// + /// Sets the default compression level to use for cabinets + /// that don't have their compression level explicitly set. + /// + public CompressionLevel DefaultCompressionLevel { private get; set; } + + public IEnumerable BackendExtensions { private get; set; } + + public Output Output { private get; set; } + + public string LayoutDirectory { private get; set; } + + public bool Compressed { private get; set; } + + public Dictionary> FileRowsByCabinet { private get; set; } + + public Func ResolveMedia { private get; set; } + + public TableDefinitionCollection TableDefinitions { private get; set; } + + public Table WixMediaTable { private get; set; } + + public IEnumerable FileTransfers => this.fileTransfers; + + /// Output to generate image for. + /// Array of files to be transfered. + /// The directory in which the image should be layed out. + /// Flag if source image should be compressed. + /// The uncompressed file rows. + public void Execute() + { + RowDictionary wixMediaRows = new RowDictionary(this.WixMediaTable); + + this.lastCabinetAddedToMediaTable = new Dictionary(); + + this.SetCabbingThreadCount(); + + // Send Binder object to Facilitate NewCabNamesCallBack Callback + CabinetBuilder cabinetBuilder = new CabinetBuilder(this.CabbingThreadCount, Marshal.GetFunctionPointerForDelegate(this.newCabNamesCallBack)); + + // Supply Compile MediaTemplate Attributes to Cabinet Builder + int MaximumCabinetSizeForLargeFileSplitting; + int MaximumUncompressedMediaSize; + this.GetMediaTemplateAttributes(out MaximumCabinetSizeForLargeFileSplitting, out MaximumUncompressedMediaSize); + cabinetBuilder.MaximumCabinetSizeForLargeFileSplitting = MaximumCabinetSizeForLargeFileSplitting; + cabinetBuilder.MaximumUncompressedMediaSize = MaximumUncompressedMediaSize; + + foreach (var entry in this.FileRowsByCabinet) + { + MediaRow mediaRow = entry.Key; + IEnumerable files = entry.Value; + CompressionLevel compressionLevel = this.DefaultCompressionLevel; + + WixMediaRow wixMediaRow = null; + string mediaLayoutFolder = null; + + if (wixMediaRows.TryGetValue(mediaRow.GetKey(), out wixMediaRow)) + { + mediaLayoutFolder = wixMediaRow.Layout; + + if (wixMediaRow.CompressionLevel.HasValue) + { + compressionLevel = wixMediaRow.CompressionLevel.Value; + } + } + + string cabinetDir = this.ResolveMedia(mediaRow, mediaLayoutFolder, this.LayoutDirectory); + + CabinetWorkItem cabinetWorkItem = this.CreateCabinetWorkItem(this.Output, cabinetDir, mediaRow, compressionLevel, files, this.fileTransfers); + if (null != cabinetWorkItem) + { + cabinetBuilder.Enqueue(cabinetWorkItem); + } + } + + // stop processing if an error previously occurred + if (Messaging.Instance.EncounteredError) + { + return; + } + + // create queued cabinets with multiple threads + cabinetBuilder.CreateQueuedCabinets(); + if (Messaging.Instance.EncounteredError) + { + return; + } + } + + /// + /// Sets the thead count to the number of processors if the current thread count is set to 0. + /// + /// The thread count value must be greater than 0 otherwise and exception will be thrown. + private void SetCabbingThreadCount() + { + // default the number of cabbing threads to the number of processors if it wasn't specified + if (0 == this.CabbingThreadCount) + { + string numberOfProcessors = System.Environment.GetEnvironmentVariable("NUMBER_OF_PROCESSORS"); + + try + { + if (null != numberOfProcessors) + { + this.CabbingThreadCount = Convert.ToInt32(numberOfProcessors, CultureInfo.InvariantCulture.NumberFormat); + + if (0 >= this.CabbingThreadCount) + { + throw new WixException(WixErrors.IllegalEnvironmentVariable("NUMBER_OF_PROCESSORS", numberOfProcessors)); + } + } + else // default to 1 if the environment variable is not set + { + this.CabbingThreadCount = 1; + } + + Messaging.Instance.OnMessage(WixVerboses.SetCabbingThreadCount(this.CabbingThreadCount.ToString())); + } + catch (ArgumentException) + { + throw new WixException(WixErrors.IllegalEnvironmentVariable("NUMBER_OF_PROCESSORS", numberOfProcessors)); + } + catch (FormatException) + { + throw new WixException(WixErrors.IllegalEnvironmentVariable("NUMBER_OF_PROCESSORS", numberOfProcessors)); + } + } + } + + + /// + /// Creates a work item to create a cabinet. + /// + /// Output for the current database. + /// Directory to create cabinet in. + /// MediaRow containing information about the cabinet. + /// Collection of files in this cabinet. + /// Array of files to be transfered. + /// created CabinetWorkItem object + private CabinetWorkItem CreateCabinetWorkItem(Output output, string cabinetDir, MediaRow mediaRow, CompressionLevel compressionLevel, IEnumerable fileFacades, List fileTransfers) + { + CabinetWorkItem cabinetWorkItem = null; + string tempCabinetFileX = Path.Combine(this.TempFilesLocation, mediaRow.Cabinet); + + // check for an empty cabinet + if (!fileFacades.Any()) + { + string cabinetName = mediaRow.Cabinet; + + // remove the leading '#' from the embedded cabinet name to make the warning easier to understand + if (cabinetName.StartsWith("#", StringComparison.Ordinal)) + { + cabinetName = cabinetName.Substring(1); + } + + // If building a patch, remind them to run -p for torch. + if (OutputType.Patch == output.Type) + { + Messaging.Instance.OnMessage(WixWarnings.EmptyCabinet(mediaRow.SourceLineNumbers, cabinetName, true)); + } + else + { + Messaging.Instance.OnMessage(WixWarnings.EmptyCabinet(mediaRow.SourceLineNumbers, cabinetName)); + } + } + + var cabinetResolver = new CabinetResolver(this.CabCachePath, this.BackendExtensions); + + ResolvedCabinet resolvedCabinet = cabinetResolver.ResolveCabinet(tempCabinetFileX, fileFacades); + + // create a cabinet work item if it's not being skipped + if (CabinetBuildOption.BuildAndCopy == resolvedCabinet.BuildOption || CabinetBuildOption.BuildAndMove == resolvedCabinet.BuildOption) + { + int maxThreshold = 0; // default to the threshold for best smartcabbing (makes smallest cabinet). + + cabinetWorkItem = new CabinetWorkItem(fileFacades, resolvedCabinet.Path, maxThreshold, compressionLevel/*, this.FileManager*/); + } + else // reuse the cabinet from the cabinet cache. + { + Messaging.Instance.OnMessage(WixVerboses.ReusingCabCache(mediaRow.SourceLineNumbers, mediaRow.Cabinet, resolvedCabinet.Path)); + + try + { + // Ensure the cached cabinet timestamp is current to prevent perpetual incremental builds. The + // problematic scenario goes like this. Imagine two cabinets in the cache. Update a file that + // goes into one of the cabinets. One cabinet will get rebuilt, the other will be copied from + // the cache. Now the file (an input) has a newer timestamp than the reused cabient (an output) + // causing the project to look like it perpetually needs a rebuild until all of the reused + // cabinets get newer timestamps. + File.SetLastWriteTime(resolvedCabinet.Path, DateTime.Now); + } + catch (Exception e) + { + Messaging.Instance.OnMessage(WixWarnings.CannotUpdateCabCache(mediaRow.SourceLineNumbers, resolvedCabinet.Path, e.Message)); + } + } + + if (mediaRow.Cabinet.StartsWith("#", StringComparison.Ordinal)) + { + Table streamsTable = output.EnsureTable(this.TableDefinitions["_Streams"]); + + Row streamRow = streamsTable.CreateRow(mediaRow.SourceLineNumbers); + streamRow[0] = mediaRow.Cabinet.Substring(1); + streamRow[1] = resolvedCabinet.Path; + } + else + { + string destinationPath = Path.Combine(cabinetDir, mediaRow.Cabinet); + FileTransfer transfer; + if (FileTransfer.TryCreate(resolvedCabinet.Path, destinationPath, CabinetBuildOption.BuildAndMove == resolvedCabinet.BuildOption, "Cabinet", mediaRow.SourceLineNumbers, out transfer)) + { + transfer.Built = true; + fileTransfers.Add(transfer); + } + } + + return cabinetWorkItem; + } + + //private ResolvedCabinet ResolveCabinet(string cabinetPath, IEnumerable fileFacades) + //{ + // ResolvedCabinet resolved = null; + + // List filesWithPath = fileFacades.Select(f => new BindFileWithPath() { Id = f.File.File, Path = f.WixFile.Source }).ToList(); + + // foreach (var extension in this.BackendExtensions) + // { + // resolved = extension.ResolveCabinet(cabinetPath, filesWithPath); + // if (null != resolved) + // { + // break; + // } + // } + + // return resolved; + //} + + /// + /// Delegate for Cabinet Split Callback + /// + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate void FileSplitCabNamesCallback([MarshalAs(UnmanagedType.LPWStr)]string firstCabName, [MarshalAs(UnmanagedType.LPWStr)]string newCabName, [MarshalAs(UnmanagedType.LPWStr)]string fileToken); + + /// + /// Call back to Add File Transfer for new Cab and add new Cab to Media table + /// This callback can come from Multiple Cabinet Builder Threads and so should be thread safe + /// This callback will not be called in case there is no File splitting. i.e. MaximumCabinetSizeForLargeFileSplitting was not authored + /// + /// The name of splitting cabinet without extention e.g. "cab1". + /// The name of the new cabinet that would be formed by splitting e.g. "cab1b.cab" + /// The file token of the first file present in the splitting cabinet + internal void NewCabNamesCallBack([MarshalAs(UnmanagedType.LPWStr)]string firstCabName, [MarshalAs(UnmanagedType.LPWStr)]string newCabName, [MarshalAs(UnmanagedType.LPWStr)]string fileToken) + { + // Locking Mutex here as this callback can come from Multiple Cabinet Builder Threads + Mutex mutex = new Mutex(false, "WixCabinetSplitBinderCallback"); + try + { + if (!mutex.WaitOne(0, false)) // Check if you can get the lock + { + // Cound not get the Lock + Messaging.Instance.OnMessage(WixVerboses.CabinetsSplitInParallel()); + mutex.WaitOne(); // Wait on other thread + } + + string firstCabinetName = firstCabName + ".cab"; + string newCabinetName = newCabName; + bool transferAdded = false; // Used for Error Handling + + // Create File Transfer for new Cabinet using transfer of Base Cabinet + foreach (FileTransfer transfer in this.FileTransfers) + { + if (firstCabinetName.Equals(Path.GetFileName(transfer.Source), StringComparison.InvariantCultureIgnoreCase)) + { + string newCabSourcePath = Path.Combine(Path.GetDirectoryName(transfer.Source), newCabinetName); + string newCabTargetPath = Path.Combine(Path.GetDirectoryName(transfer.Destination), newCabinetName); + + FileTransfer newTransfer; + if (FileTransfer.TryCreate(newCabSourcePath, newCabTargetPath, transfer.Move, "Cabinet", transfer.SourceLineNumbers, out newTransfer)) + { + newTransfer.Built = true; + this.fileTransfers.Add(newTransfer); + transferAdded = true; + break; + } + } + } + + // Check if File Transfer was added + if (!transferAdded) + { + throw new WixException(WixErrors.SplitCabinetCopyRegistrationFailed(newCabinetName, firstCabinetName)); + } + + // Add the new Cabinets to media table using LastSequence of Base Cabinet + Table mediaTable = this.Output.Tables["Media"]; + Table wixFileTable = this.Output.Tables["WixFile"]; + int diskIDForLastSplitCabAdded = 0; // The DiskID value for the first cab in this cabinet split chain + int lastSequenceForLastSplitCabAdded = 0; // The LastSequence value for the first cab in this cabinet split chain + bool lastSplitCabinetFound = false; // Used for Error Handling + + string lastCabinetOfThisSequence = String.Empty; + // Get the Value of Last Cabinet Added in this split Sequence from Dictionary + if (!this.lastCabinetAddedToMediaTable.TryGetValue(firstCabinetName, out lastCabinetOfThisSequence)) + { + // If there is no value for this sequence, then use first Cabinet is the last one of this split sequence + lastCabinetOfThisSequence = firstCabinetName; + } + + foreach (MediaRow mediaRow in mediaTable.Rows) + { + // Get details for the Last Cabinet Added in this Split Sequence + if ((lastSequenceForLastSplitCabAdded == 0) && lastCabinetOfThisSequence.Equals(mediaRow.Cabinet, StringComparison.InvariantCultureIgnoreCase)) + { + lastSequenceForLastSplitCabAdded = mediaRow.LastSequence; + diskIDForLastSplitCabAdded = mediaRow.DiskId; + lastSplitCabinetFound = true; + } + + // Check for Name Collision for the new Cabinet added + if (newCabinetName.Equals(mediaRow.Cabinet, StringComparison.InvariantCultureIgnoreCase)) + { + // Name Collision of generated Split Cabinet Name and user Specified Cab name for current row + throw new WixException(WixErrors.SplitCabinetNameCollision(newCabinetName, firstCabinetName)); + } + } + + // Check if the last Split Cabinet was found in the Media Table + if (!lastSplitCabinetFound) + { + throw new WixException(WixErrors.SplitCabinetInsertionFailed(newCabinetName, firstCabinetName, lastCabinetOfThisSequence)); + } + + // The new Row has to be inserted just after the last cab in this cabinet split chain according to DiskID Sort + // This is because the FDI Extract requires DiskID of Split Cabinets to be continuous. It Fails otherwise with + // Error 2350 (FDI Server Error) as next DiskID did not have the right split cabinet during extraction + MediaRow newMediaRow = (MediaRow)mediaTable.CreateRow(null); + newMediaRow.Cabinet = newCabinetName; + newMediaRow.DiskId = diskIDForLastSplitCabAdded + 1; // When Sorted with DiskID, this new Cabinet Row is an Insertion + newMediaRow.LastSequence = lastSequenceForLastSplitCabAdded; + + // Now increment the DiskID for all rows that come after the newly inserted row to Ensure that DiskId is unique + foreach (MediaRow mediaRow in mediaTable.Rows) + { + // Check if this row comes after inserted row and it is not the new cabinet inserted row + if (mediaRow.DiskId >= newMediaRow.DiskId && !newCabinetName.Equals(mediaRow.Cabinet, StringComparison.InvariantCultureIgnoreCase)) + { + mediaRow.DiskId++; // Increment DiskID + } + } + + // Now Increment DiskID for All files Rows so that they refer to the right Media Row + foreach (WixFileRow wixFileRow in wixFileTable.Rows) + { + // Check if this row comes after inserted row and if this row is not the file that has to go into the current cabinet + // This check will work as we have only one large file in every splitting cabinet + // If we want to support splitting cabinet with more large files we need to update this code + if (wixFileRow.DiskId >= newMediaRow.DiskId && !wixFileRow.File.Equals(fileToken, StringComparison.InvariantCultureIgnoreCase)) + { + wixFileRow.DiskId++; // Increment DiskID + } + } + + // Update the Last Cabinet Added in the Split Sequence in Dictionary for future callback + this.lastCabinetAddedToMediaTable[firstCabinetName] = newCabinetName; + + mediaTable.ValidateRows(); // Valdiates DiskDIs, throws Exception as Wix Error if validation fails + } + finally + { + // Releasing the Mutex here + mutex.ReleaseMutex(); + } + } + + + /// + /// Gets Compiler Values of MediaTemplate Attributes governing Maximum Cabinet Size after applying Environment Variable Overrides + /// + /// Output to generate image for. + /// The indexed file rows. + private void GetMediaTemplateAttributes(out int maxCabSizeForLargeFileSplitting, out int maxUncompressedMediaSize) + { + // Get Environment Variable Overrides for MediaTemplate Attributes governing Maximum Cabinet Size + string mcslfsString = Environment.GetEnvironmentVariable("WIX_MCSLFS"); + string mumsString = Environment.GetEnvironmentVariable("WIX_MUMS"); + int maxCabSizeForLargeFileInMB = 0; + int maxPreCompressedSizeInMB = 0; + ulong testOverFlow = 0; + + // Supply Compile MediaTemplate Attributes to Cabinet Builder + Table mediaTemplateTable = this.Output.Tables["WixMediaTemplate"]; + if (mediaTemplateTable != null) + { + WixMediaTemplateRow mediaTemplateRow = (WixMediaTemplateRow)mediaTemplateTable.Rows[0]; + + // Get the Value for Max Cab Size for File Splitting + try + { + // Override authored mcslfs value if environment variable is authored. + if (!String.IsNullOrEmpty(mcslfsString)) + { + maxCabSizeForLargeFileInMB = Int32.Parse(mcslfsString); + } + else + { + maxCabSizeForLargeFileInMB = mediaTemplateRow.MaximumCabinetSizeForLargeFileSplitting; + } + testOverFlow = (ulong)maxCabSizeForLargeFileInMB * 1024 * 1024; + } + catch (FormatException) + { + throw new WixException(WixErrors.IllegalEnvironmentVariable("WIX_MCSLFS", mcslfsString)); + } + catch (OverflowException) + { + throw new WixException(WixErrors.MaximumCabinetSizeForLargeFileSplittingTooLarge(null, maxCabSizeForLargeFileInMB, MaxValueOfMaxCabSizeForLargeFileSplitting)); + } + + try + { + // Override authored mums value if environment variable is authored. + if (!String.IsNullOrEmpty(mumsString)) + { + maxPreCompressedSizeInMB = Int32.Parse(mumsString); + } + else + { + maxPreCompressedSizeInMB = mediaTemplateRow.MaximumUncompressedMediaSize; + } + testOverFlow = (ulong)maxPreCompressedSizeInMB * 1024 * 1024; + } + catch (FormatException) + { + throw new WixException(WixErrors.IllegalEnvironmentVariable("WIX_MUMS", mumsString)); + } + catch (OverflowException) + { + throw new WixException(WixErrors.MaximumUncompressedMediaSizeTooLarge(null, maxPreCompressedSizeInMB)); + } + + maxCabSizeForLargeFileSplitting = maxCabSizeForLargeFileInMB; + maxUncompressedMediaSize = maxPreCompressedSizeInMB; + } + else + { + maxCabSizeForLargeFileSplitting = 0; + maxUncompressedMediaSize = DefaultMaximumUncompressedMediaSize; + } + } + } +} diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/CreateDeltaPatchesCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/CreateDeltaPatchesCommand.cs new file mode 100644 index 00000000..767671b8 --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Bind/CreateDeltaPatchesCommand.cs @@ -0,0 +1,87 @@ +// 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.Databases +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using WixToolset.Core.Bind; + using WixToolset.Data; + using WixToolset.Data.Rows; + + /// + /// Creates delta patches and updates the appropriate rows to point to the newly generated patches. + /// + internal class CreateDeltaPatchesCommand + { + public IEnumerable FileFacades { private get; set; } + + public Table WixPatchIdTable { private get; set; } + + public string TempFilesLocation { private get; set; } + + public void Execute() + { + bool optimizePatchSizeForLargeFiles = false; + PatchSymbolFlagsType apiPatchingSymbolFlags = 0; + + if (null != this.WixPatchIdTable) + { + Row row = this.WixPatchIdTable.Rows[0]; + if (null != row) + { + if (null != row[2]) + { + optimizePatchSizeForLargeFiles = (1 == Convert.ToUInt32(row[2], CultureInfo.InvariantCulture)); + } + + if (null != row[3]) + { + apiPatchingSymbolFlags = (PatchSymbolFlagsType)Convert.ToUInt32(row[3], CultureInfo.InvariantCulture); + } + } + } + + foreach (FileFacade facade in this.FileFacades) + { + if (RowOperation.Modify == facade.File.Operation && + 0 != (facade.WixFile.PatchAttributes & PatchAttributeType.IncludeWholeFile)) + { + string deltaBase = String.Concat("delta_", facade.File.File); + string deltaFile = Path.Combine(this.TempFilesLocation, String.Concat(deltaBase, ".dpf")); + string headerFile = Path.Combine(this.TempFilesLocation, String.Concat(deltaBase, ".phd")); + + bool retainRangeWarning = false; + + if (PatchAPI.PatchInterop.CreateDelta( + deltaFile, + facade.WixFile.Source, + facade.DeltaPatchFile.Symbols, + facade.DeltaPatchFile.RetainOffsets, + new[] { facade.WixFile.PreviousSource }, + facade.DeltaPatchFile.PreviousSymbols.Split(new[] { ';' }), + facade.DeltaPatchFile.PreviousIgnoreLengths.Split(new[] { ';' }), + facade.DeltaPatchFile.PreviousIgnoreOffsets.Split(new[] { ';' }), + facade.DeltaPatchFile.PreviousRetainLengths.Split(new[] { ';' }), + facade.DeltaPatchFile.PreviousRetainOffsets.Split(new[] { ';' }), + apiPatchingSymbolFlags, + optimizePatchSizeForLargeFiles, + out retainRangeWarning)) + { + PatchAPI.PatchInterop.ExtractDeltaHeader(deltaFile, headerFile); + + facade.WixFile.Source = deltaFile; + facade.WixFile.DeltaPatchHeaderSource = headerFile; + } + + if (retainRangeWarning) + { + // TODO: get patch family to add to warning message for PatchWiz parity. + Messaging.Instance.OnMessage(WixWarnings.RetainRangeMismatch(facade.File.SourceLineNumbers, facade.File.File)); + } + } + } + } + } +} diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/CreateSpecialPropertiesCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/CreateSpecialPropertiesCommand.cs new file mode 100644 index 00000000..aef130b0 --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Bind/CreateSpecialPropertiesCommand.cs @@ -0,0 +1,68 @@ +// 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.Databases +{ + using System; + using System.Collections.Generic; + using WixToolset.Data; + using WixToolset.Data.Rows; + + internal class CreateSpecialPropertiesCommand + { + public Table PropertyTable { private get; set; } + + public Table WixPropertyTable { private get; set; } + + public void Execute() + { + // Create the special properties. + if (null != this.WixPropertyTable) + { + // Create lists of the properties that contribute to the special lists of properties. + SortedSet adminProperties = new SortedSet(); + SortedSet secureProperties = new SortedSet(); + SortedSet hiddenProperties = new SortedSet(); + + foreach (WixPropertyRow wixPropertyRow in this.WixPropertyTable.Rows) + { + if (wixPropertyRow.Admin) + { + adminProperties.Add(wixPropertyRow.Id); + } + + if (wixPropertyRow.Hidden) + { + hiddenProperties.Add(wixPropertyRow.Id); + } + + if (wixPropertyRow.Secure) + { + secureProperties.Add(wixPropertyRow.Id); + } + } + + Table propertyTable = this.PropertyTable; + if (0 < adminProperties.Count) + { + PropertyRow row = (PropertyRow)propertyTable.CreateRow(null); + row.Property = "AdminProperties"; + row.Value = String.Join(";", adminProperties); + } + + if (0 < secureProperties.Count) + { + PropertyRow row = (PropertyRow)propertyTable.CreateRow(null); + row.Property = "SecureCustomProperties"; + row.Value = String.Join(";", secureProperties); + } + + if (0 < hiddenProperties.Count) + { + PropertyRow row = (PropertyRow)propertyTable.CreateRow(null); + row.Property = "MsiHiddenProperties"; + row.Value = String.Join(";", hiddenProperties); + } + } + } + } +} diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/ExtractMergeModuleFilesCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/ExtractMergeModuleFilesCommand.cs new file mode 100644 index 00000000..ae76037d --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Bind/ExtractMergeModuleFilesCommand.cs @@ -0,0 +1,226 @@ +// 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.Databases +{ + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Runtime.InteropServices; + using WixToolset.Data; + using WixToolset.Data.Rows; + using WixToolset.MergeMod; + using WixToolset.Msi; + using WixToolset.Core.Native; + using WixToolset.Core.Bind; + using WixToolset.Core.Cab; + + /// + /// Retrieve files information and extract them from merge modules. + /// + internal class ExtractMergeModuleFilesCommand + { + public IEnumerable FileFacades { private get; set; } + + public Table FileTable { private get; set; } + + public Table WixFileTable { private get; set; } + + public Table WixMergeTable { private get; set; } + + public int OutputInstallerVersion { private get; set; } + + public bool SuppressLayout { private get; set; } + + public string TempFilesLocation { private get; set; } + + public IEnumerable MergeModulesFileFacades { get; private set; } + + public void Execute() + { + List mergeModulesFileFacades = new List(); + + IMsmMerge2 merge = MsmInterop.GetMsmMerge(); + + // Index all of the file rows to be able to detect collisions with files in the Merge Modules. + // It may seem a bit expensive to build up this index solely for the purpose of checking collisions + // and you may be thinking, "Surely, we must need the file rows indexed elsewhere." It turns out + // there are other cases where we need all the file rows indexed, however they are not common cases. + // Now since Merge Modules are already slow and generally less desirable than .wixlibs we'll let + // this case be slightly more expensive because the cost of maintaining an indexed file row collection + // is a lot more costly for the common cases. + Dictionary indexedFileFacades = this.FileFacades.ToDictionary(f => f.File.File, StringComparer.Ordinal); + + foreach (WixMergeRow wixMergeRow in this.WixMergeTable.Rows) + { + bool containsFiles = this.CreateFacadesForMergeModuleFiles(wixMergeRow, mergeModulesFileFacades, indexedFileFacades); + + // If the module has files and creating layout + if (containsFiles && !this.SuppressLayout) + { + this.ExtractFilesFromMergeModule(merge, wixMergeRow); + } + } + + this.MergeModulesFileFacades = mergeModulesFileFacades; + } + + private bool CreateFacadesForMergeModuleFiles(WixMergeRow wixMergeRow, List mergeModulesFileFacades, Dictionary indexedFileFacades) + { + bool containsFiles = false; + + try + { + // read the module's File table to get its FileMediaInformation entries and gather any other information needed from the module. + using (Database db = new Database(wixMergeRow.SourceFile, OpenDatabase.ReadOnly)) + { + if (db.TableExists("File") && db.TableExists("Component")) + { + Dictionary uniqueModuleFileIdentifiers = new Dictionary(StringComparer.OrdinalIgnoreCase); + + using (View view = db.OpenExecuteView("SELECT `File`, `Directory_` FROM `File`, `Component` WHERE `Component_`=`Component`")) + { + // add each file row from the merge module into the file row collection (check for errors along the way) + while (true) + { + using (Record record = view.Fetch()) + { + if (null == record) + { + break; + } + + // NOTE: this is very tricky - the merge module file rows are not added to the + // file table because they should not be created via idt import. Instead, these + // rows are created by merging in the actual modules. + FileRow fileRow = (FileRow)this.FileTable.CreateRow(wixMergeRow.SourceLineNumbers, false); + fileRow.File = record[1]; + fileRow.Compressed = wixMergeRow.FileCompression; + + WixFileRow wixFileRow = (WixFileRow)this.WixFileTable.CreateRow(wixMergeRow.SourceLineNumbers, false); + wixFileRow.Directory = record[2]; + wixFileRow.DiskId = wixMergeRow.DiskId; + wixFileRow.PatchGroup = -1; + wixFileRow.Source = String.Concat(this.TempFilesLocation, Path.DirectorySeparatorChar, "MergeId.", wixMergeRow.Number.ToString(CultureInfo.InvariantCulture), Path.DirectorySeparatorChar, record[1]); + + FileFacade mergeModuleFileFacade = new FileFacade(true, fileRow, wixFileRow); + + FileFacade collidingFacade; + + // If case-sensitive collision with another merge module or a user-authored file identifier. + if (indexedFileFacades.TryGetValue(mergeModuleFileFacade.File.File, out collidingFacade)) + { + Messaging.Instance.OnMessage(WixErrors.DuplicateModuleFileIdentifier(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, collidingFacade.File.File)); + } + else if (uniqueModuleFileIdentifiers.TryGetValue(mergeModuleFileFacade.File.File, out collidingFacade)) // case-insensitive collision with another file identifier in the same merge module + { + Messaging.Instance.OnMessage(WixErrors.DuplicateModuleCaseInsensitiveFileIdentifier(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, mergeModuleFileFacade.File.File, collidingFacade.File.File)); + } + else // no collision + { + mergeModulesFileFacades.Add(mergeModuleFileFacade); + + // Keep updating the indexes as new rows are added. + indexedFileFacades.Add(mergeModuleFileFacade.File.File, mergeModuleFileFacade); + uniqueModuleFileIdentifiers.Add(mergeModuleFileFacade.File.File, mergeModuleFileFacade); + } + + containsFiles = true; + } + } + } + } + + // Get the summary information to detect the Schema + using (SummaryInformation summaryInformation = new SummaryInformation(db)) + { + string moduleInstallerVersionString = summaryInformation.GetProperty(14); + + try + { + int moduleInstallerVersion = Convert.ToInt32(moduleInstallerVersionString, CultureInfo.InvariantCulture); + if (moduleInstallerVersion > this.OutputInstallerVersion) + { + Messaging.Instance.OnMessage(WixWarnings.InvalidHigherInstallerVersionInModule(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, moduleInstallerVersion, this.OutputInstallerVersion)); + } + } + catch (FormatException) + { + throw new WixException(WixErrors.MissingOrInvalidModuleInstallerVersion(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, wixMergeRow.SourceFile, moduleInstallerVersionString)); + } + } + } + } + catch (FileNotFoundException) + { + throw new WixException(WixErrors.FileNotFound(wixMergeRow.SourceLineNumbers, wixMergeRow.SourceFile)); + } + catch (Win32Exception) + { + throw new WixException(WixErrors.CannotOpenMergeModule(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, wixMergeRow.SourceFile)); + } + + return containsFiles; + } + + private void ExtractFilesFromMergeModule(IMsmMerge2 merge, WixMergeRow wixMergeRow) + { + bool moduleOpen = false; + short mergeLanguage; + + try + { + mergeLanguage = Convert.ToInt16(wixMergeRow.Language, CultureInfo.InvariantCulture); + } + catch (System.FormatException) + { + Messaging.Instance.OnMessage(WixErrors.InvalidMergeLanguage(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, wixMergeRow.Language)); + return; + } + + try + { + merge.OpenModule(wixMergeRow.SourceFile, mergeLanguage); + moduleOpen = true; + + string safeMergeId = wixMergeRow.Number.ToString(CultureInfo.InvariantCulture.NumberFormat); + + // extract the module cabinet, then explode all of the files to a temp directory + string moduleCabPath = String.Concat(this.TempFilesLocation, Path.DirectorySeparatorChar, safeMergeId, ".module.cab"); + merge.ExtractCAB(moduleCabPath); + + string mergeIdPath = String.Concat(this.TempFilesLocation, Path.DirectorySeparatorChar, "MergeId.", safeMergeId); + Directory.CreateDirectory(mergeIdPath); + + using (var extractCab = new WixExtractCab()) + { + try + { + extractCab.Extract(moduleCabPath, mergeIdPath); + } + catch (FileNotFoundException) + { + throw new WixException(WixErrors.CabFileDoesNotExist(moduleCabPath, wixMergeRow.SourceFile, mergeIdPath)); + } + catch + { + throw new WixException(WixErrors.CabExtractionFailed(moduleCabPath, wixMergeRow.SourceFile, mergeIdPath)); + } + } + } + catch (COMException ce) + { + throw new WixException(WixErrors.UnableToOpenModule(wixMergeRow.SourceLineNumbers, wixMergeRow.SourceFile, ce.Message)); + } + finally + { + if (moduleOpen) + { + merge.CloseModule(); + } + } + } + } +} diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/GenerateDatabaseCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/GenerateDatabaseCommand.cs new file mode 100644 index 00000000..26d254f2 --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Bind/GenerateDatabaseCommand.cs @@ -0,0 +1,332 @@ +// 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.Databases +{ + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Globalization; + using System.IO; + using System.Text; + using WixToolset.Data; + using WixToolset.Extensibility; + using WixToolset.Msi; + using WixToolset.Core.Native; + + internal class GenerateDatabaseCommand + { + public int Codepage { private get; set; } + + public IEnumerable Extensions { private get; set; } + + /// + /// Whether to keep columns added in a transform. + /// + public bool KeepAddedColumns { private get; set; } + + public Output Output { private get; set; } + + public string OutputPath { private get; set; } + + public TableDefinitionCollection TableDefinitions { private get; set; } + + public string TempFilesLocation { private get; set; } + + /// + /// Whether to use a subdirectory based on the file name for intermediate files. + /// + public bool SuppressAddingValidationRows { private get; set; } + + public bool UseSubDirectory { private get; set; } + + public void Execute() + { + // Add the _Validation rows. + if (!this.SuppressAddingValidationRows) + { + Table validationTable = this.Output.EnsureTable(this.TableDefinitions["_Validation"]); + + foreach (Table table in this.Output.Tables) + { + if (!table.Definition.Unreal) + { + // Add the validation rows for this table. + table.Definition.AddValidationRows(validationTable); + } + } + } + + // Set the base directory. + string baseDirectory = this.TempFilesLocation; + + if (this.UseSubDirectory) + { + string filename = Path.GetFileNameWithoutExtension(this.OutputPath); + baseDirectory = Path.Combine(baseDirectory, filename); + + // make sure the directory exists + Directory.CreateDirectory(baseDirectory); + } + + try + { + OpenDatabase type = OpenDatabase.CreateDirect; + + // set special flag for patch files + if (OutputType.Patch == this.Output.Type) + { + type |= OpenDatabase.OpenPatchFile; + } + +#if DEBUG + Console.WriteLine("Opening database at: {0}", this.OutputPath); +#endif + + using (Database db = new Database(this.OutputPath, type)) + { + // Localize the codepage if a value was specified directly. + if (-1 != this.Codepage) + { + this.Output.Codepage = this.Codepage; + } + + // if we're not using the default codepage, import a new one into our + // database before we add any tables (or the tables would be added + // with the wrong codepage). + if (0 != this.Output.Codepage) + { + this.SetDatabaseCodepage(db, this.Output.Codepage); + } + + foreach (Table table in this.Output.Tables) + { + Table importTable = table; + bool hasBinaryColumn = false; + + // Skip all unreal tables other than _Streams. + if (table.Definition.Unreal && "_Streams" != table.Name) + { + continue; + } + + // Do not put the _Validation table in patches, it is not needed. + if (OutputType.Patch == this.Output.Type && "_Validation" == table.Name) + { + continue; + } + + // The only way to import binary data is to copy it to a local subdirectory first. + // To avoid this extra copying and perf hit, import an empty table with the same + // definition and later import the binary data from source using records. + foreach (ColumnDefinition columnDefinition in table.Definition.Columns) + { + if (ColumnType.Object == columnDefinition.Type) + { + importTable = new Table(table.Section, table.Definition); + hasBinaryColumn = true; + break; + } + } + + // Create the table via IDT import. + if ("_Streams" != importTable.Name) + { + try + { + db.ImportTable(this.Output.Codepage, importTable, baseDirectory, this.KeepAddedColumns); + } + catch (WixInvalidIdtException) + { + // If ValidateRows finds anything it doesn't like, it throws + importTable.ValidateRows(); + + // Otherwise we rethrow the InvalidIdt + throw; + } + } + + // insert the rows via SQL query if this table contains object fields + if (hasBinaryColumn) + { + StringBuilder query = new StringBuilder("SELECT "); + + // Build the query for the view. + bool firstColumn = true; + foreach (ColumnDefinition columnDefinition in table.Definition.Columns) + { + if (!firstColumn) + { + query.Append(","); + } + + query.AppendFormat(" `{0}`", columnDefinition.Name); + firstColumn = false; + } + query.AppendFormat(" FROM `{0}`", table.Name); + + using (View tableView = db.OpenExecuteView(query.ToString())) + { + // Import each row containing a stream + foreach (Row row in table.Rows) + { + using (Record record = new Record(table.Definition.Columns.Count)) + { + StringBuilder streamName = new StringBuilder(); + bool needStream = false; + + // the _Streams table doesn't prepend the table name (or a period) + if ("_Streams" != table.Name) + { + streamName.Append(table.Name); + } + + for (int i = 0; i < table.Definition.Columns.Count; i++) + { + ColumnDefinition columnDefinition = table.Definition.Columns[i]; + + switch (columnDefinition.Type) + { + case ColumnType.Localized: + case ColumnType.Preserved: + case ColumnType.String: + if (columnDefinition.PrimaryKey) + { + if (0 < streamName.Length) + { + streamName.Append("."); + } + streamName.Append((string)row[i]); + } + + record.SetString(i + 1, (string)row[i]); + break; + case ColumnType.Number: + record.SetInteger(i + 1, Convert.ToInt32(row[i], CultureInfo.InvariantCulture)); + break; + case ColumnType.Object: + if (null != row[i]) + { + needStream = true; + try + { + record.SetStream(i + 1, (string)row[i]); + } + catch (Win32Exception e) + { + if (0xA1 == e.NativeErrorCode) // ERROR_BAD_PATHNAME + { + throw new WixException(WixErrors.FileNotFound(row.SourceLineNumbers, (string)row[i])); + } + else + { + throw new WixException(WixErrors.Win32Exception(e.NativeErrorCode, e.Message)); + } + } + } + break; + } + } + + // stream names are created by concatenating the name of the table with the values + // of the primary key (delimited by periods) + // check for a stream name that is more than 62 characters long (the maximum allowed length) + if (needStream && MsiInterop.MsiMaxStreamNameLength < streamName.Length) + { + Messaging.Instance.OnMessage(WixErrors.StreamNameTooLong(row.SourceLineNumbers, table.Name, streamName.ToString(), streamName.Length)); + } + else // add the row to the database + { + tableView.Modify(ModifyView.Assign, record); + } + } + } + } + + // Remove rows from the _Streams table for wixpdbs. + if ("_Streams" == table.Name) + { + table.Rows.Clear(); + } + } + } + + // Insert substorages (usually transforms inside a patch or instance transforms in a package). + if (0 < this.Output.SubStorages.Count) + { + using (View storagesView = new View(db, "SELECT `Name`, `Data` FROM `_Storages`")) + { + foreach (SubStorage subStorage in this.Output.SubStorages) + { + string transformFile = Path.Combine(this.TempFilesLocation, String.Concat(subStorage.Name, ".mst")); + + // Bind the transform. + this.BindTransform(subStorage.Data, transformFile); + + if (Messaging.Instance.EncounteredError) + { + continue; + } + + // add the storage + using (Record record = new Record(2)) + { + record.SetString(1, subStorage.Name); + record.SetStream(2, transformFile); + storagesView.Modify(ModifyView.Assign, record); + } + } + } + } + + // We're good, commit the changes to the new database. + db.Commit(); + } + } + catch (IOException) + { + // TODO: this error message doesn't seem specific enough + throw new WixFileNotFoundException(new SourceLineNumber(this.OutputPath), this.OutputPath); + } + } + + private void BindTransform(Output transform, string outputPath) + { + BindTransformCommand command = new BindTransformCommand(); + command.Extensions = this.Extensions; + command.TempFilesLocation = this.TempFilesLocation; + command.Transform = transform; + command.OutputPath = outputPath; + command.TableDefinitions = this.TableDefinitions; + command.Execute(); + } + + /// + /// Sets the codepage of a database. + /// + /// Database to set codepage into. + /// Output with the codepage for the database. + private void SetDatabaseCodepage(Database db, int codepage) + { + // write out the _ForceCodepage IDT file + string idtPath = Path.Combine(this.TempFilesLocation, "_ForceCodepage.idt"); + using (StreamWriter idtFile = new StreamWriter(idtPath, false, Encoding.ASCII)) + { + idtFile.WriteLine(); // dummy column name record + idtFile.WriteLine(); // dummy column definition record + idtFile.Write(codepage); + idtFile.WriteLine("\t_ForceCodepage"); + } + + // try to import the table into the MSI + try + { + db.Import(idtPath); + } + catch (WixInvalidIdtException) + { + // the IDT should be valid, so an invalid code page was given + throw new WixException(WixErrors.IllegalCodepage(codepage)); + } + } + } +} diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesCommand.cs new file mode 100644 index 00000000..caf8b7a7 --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Bind/GetFileFacadesCommand.cs @@ -0,0 +1,149 @@ +// 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.Databases +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using WixToolset.Core.Bind; + using WixToolset.Data; + using WixToolset.Data.Rows; + + internal class GetFileFacadesCommand + { + public Table FileTable { private get; set; } + + public Table WixFileTable { private get; set; } + + public Table WixDeltaPatchFileTable { private get; set; } + + public Table WixDeltaPatchSymbolPathsTable { private get; set; } + + public List FileFacades { get; private set; } + + public void Execute() + { + List facades = new List(this.FileTable.Rows.Count); + + RowDictionary wixFiles = new RowDictionary(this.WixFileTable); + RowDictionary deltaPatchFiles = new RowDictionary(this.WixDeltaPatchFileTable); + + foreach (FileRow file in this.FileTable.Rows) + { + WixDeltaPatchFileRow deltaPatchFile = null; + + deltaPatchFiles.TryGetValue(file.File, out deltaPatchFile); + + facades.Add(new FileFacade(file, wixFiles[file.File], deltaPatchFile)); + } + + if (null != this.WixDeltaPatchSymbolPathsTable) + { + this.ResolveDeltaPatchSymbolPaths(deltaPatchFiles, facades); + } + + this.FileFacades = facades; + } + + /// + /// Merge data from the WixPatchSymbolPaths rows into the WixDeltaPatchFile rows. + /// + public RowDictionary ResolveDeltaPatchSymbolPaths(RowDictionary deltaPatchFiles, IEnumerable facades) + { + ILookup filesByComponent = null; + ILookup filesByDirectory = null; + ILookup filesByDiskId = null; + + foreach (WixDeltaPatchSymbolPathsRow row in this.WixDeltaPatchSymbolPathsTable.RowsAs().OrderBy(r => r.Type)) + { + switch (row.Type) + { + case SymbolPathType.File: + this.MergeSymbolPaths(row, deltaPatchFiles[row.Id]); + break; + + case SymbolPathType.Component: + if (null == filesByComponent) + { + filesByComponent = facades.ToLookup(f => f.File.Component); + } + + foreach (FileFacade facade in filesByComponent[row.Id]) + { + this.MergeSymbolPaths(row, deltaPatchFiles[facade.File.File]); + } + break; + + case SymbolPathType.Directory: + if (null == filesByDirectory) + { + filesByDirectory = facades.ToLookup(f => f.WixFile.Directory); + } + + foreach (FileFacade facade in filesByDirectory[row.Id]) + { + this.MergeSymbolPaths(row, deltaPatchFiles[facade.File.File]); + } + break; + + case SymbolPathType.Media: + if (null == filesByDiskId) + { + filesByDiskId = facades.ToLookup(f => f.WixFile.DiskId.ToString(CultureInfo.InvariantCulture)); + } + + foreach (FileFacade facade in filesByDiskId[row.Id]) + { + this.MergeSymbolPaths(row, deltaPatchFiles[facade.File.File]); + } + break; + + case SymbolPathType.Product: + foreach (WixDeltaPatchFileRow fileRow in deltaPatchFiles.Values) + { + this.MergeSymbolPaths(row, fileRow); + } + break; + + default: + // error + break; + } + } + + return deltaPatchFiles; + } + + /// + /// Merge data from a row in the WixPatchSymbolsPaths table into an associated WixDeltaPatchFile row. + /// + /// Row from the WixPatchSymbolsPaths table. + /// FileRow into which to set symbol information. + /// This includes PreviousData as well. + private void MergeSymbolPaths(WixDeltaPatchSymbolPathsRow row, WixDeltaPatchFileRow file) + { + if (null == file.Symbols) + { + file.Symbols = row.SymbolPaths; + } + else + { + file.Symbols = String.Concat(file.Symbols, ";", row.SymbolPaths); + } + + Field field = row.Fields[2]; + if (null != field.PreviousData) + { + if (null == file.PreviousSymbols) + { + file.PreviousSymbols = field.PreviousData; + } + else + { + file.PreviousSymbols = String.Concat(file.PreviousSymbols, ";", field.PreviousData); + } + } + } + } +} diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/MergeModulesCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/MergeModulesCommand.cs new file mode 100644 index 00000000..624cbb43 --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Bind/MergeModulesCommand.cs @@ -0,0 +1,351 @@ +// 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.Databases +{ + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.ComponentModel; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Runtime.InteropServices; + using System.Text; + using System.Xml; + using System.Xml.XPath; + using WixToolset.Clr.Interop; + using WixToolset.Data; + using WixToolset.Data.Rows; + using WixToolset.MergeMod; + using WixToolset.Msi; + using WixToolset.Core.Native; + using WixToolset.Core.Bind; + + /// + /// Update file information. + /// + internal class MergeModulesCommand + { + public IEnumerable FileFacades { private get; set; } + + public Output Output { private get; set; } + + public string OutputPath { private get; set; } + + public IEnumerable SuppressedTableNames { private get; set; } + + public string TempFilesLocation { private get; set; } + + public void Execute() + { + Debug.Assert(OutputType.Product == this.Output.Type); + + Table wixMergeTable = this.Output.Tables["WixMerge"]; + Table wixFeatureModulesTable = this.Output.Tables["WixFeatureModules"]; + + // check for merge rows to see if there is any work to do + if (null == wixMergeTable || 0 == wixMergeTable.Rows.Count) + { + return; + } + + IMsmMerge2 merge = null; + bool commit = true; + bool logOpen = false; + bool databaseOpen = false; + string logPath = null; + try + { + merge = MsmInterop.GetMsmMerge(); + + logPath = Path.Combine(this.TempFilesLocation, "merge.log"); + merge.OpenLog(logPath); + logOpen = true; + + merge.OpenDatabase(this.OutputPath); + databaseOpen = true; + + // process all the merge rows + foreach (WixMergeRow wixMergeRow in wixMergeTable.Rows) + { + bool moduleOpen = false; + + try + { + short mergeLanguage; + + try + { + mergeLanguage = Convert.ToInt16(wixMergeRow.Language, CultureInfo.InvariantCulture); + } + catch (System.FormatException) + { + Messaging.Instance.OnMessage(WixErrors.InvalidMergeLanguage(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, wixMergeRow.Language)); + continue; + } + + Messaging.Instance.OnMessage(WixVerboses.OpeningMergeModule(wixMergeRow.SourceFile, mergeLanguage)); + merge.OpenModule(wixMergeRow.SourceFile, mergeLanguage); + moduleOpen = true; + + // If there is merge configuration data, create a callback object to contain it all. + ConfigurationCallback callback = null; + if (!String.IsNullOrEmpty(wixMergeRow.ConfigurationData)) + { + callback = new ConfigurationCallback(wixMergeRow.ConfigurationData); + } + + // merge the module into the database that's being built + Messaging.Instance.OnMessage(WixVerboses.MergingMergeModule(wixMergeRow.SourceFile)); + merge.MergeEx(wixMergeRow.Feature, wixMergeRow.Directory, callback); + + // connect any non-primary features + if (null != wixFeatureModulesTable) + { + foreach (Row row in wixFeatureModulesTable.Rows) + { + if (wixMergeRow.Id == (string)row[1]) + { + Messaging.Instance.OnMessage(WixVerboses.ConnectingMergeModule(wixMergeRow.SourceFile, (string)row[0])); + merge.Connect((string)row[0]); + } + } + } + } + catch (COMException) + { + commit = false; + } + finally + { + IMsmErrors mergeErrors = merge.Errors; + + // display all the errors encountered during the merge operations for this module + for (int i = 1; i <= mergeErrors.Count; i++) + { + IMsmError mergeError = mergeErrors[i]; + StringBuilder databaseKeys = new StringBuilder(); + StringBuilder moduleKeys = new StringBuilder(); + + // build a string of the database keys + for (int j = 1; j <= mergeError.DatabaseKeys.Count; j++) + { + if (1 != j) + { + databaseKeys.Append(';'); + } + databaseKeys.Append(mergeError.DatabaseKeys[j]); + } + + // build a string of the module keys + for (int j = 1; j <= mergeError.ModuleKeys.Count; j++) + { + if (1 != j) + { + moduleKeys.Append(';'); + } + moduleKeys.Append(mergeError.ModuleKeys[j]); + } + + // display the merge error based on the msm error type + switch (mergeError.Type) + { + case MsmErrorType.msmErrorExclusion: + Messaging.Instance.OnMessage(WixErrors.MergeExcludedModule(wixMergeRow.SourceLineNumbers, wixMergeRow.Id, moduleKeys.ToString())); + break; + case MsmErrorType.msmErrorFeatureRequired: + Messaging.Instance.OnMessage(WixErrors.MergeFeatureRequired(wixMergeRow.SourceLineNumbers, mergeError.ModuleTable, moduleKeys.ToString(), wixMergeRow.SourceFile, wixMergeRow.Id)); + break; + case MsmErrorType.msmErrorLanguageFailed: + Messaging.Instance.OnMessage(WixErrors.MergeLanguageFailed(wixMergeRow.SourceLineNumbers, mergeError.Language, wixMergeRow.SourceFile)); + break; + case MsmErrorType.msmErrorLanguageUnsupported: + Messaging.Instance.OnMessage(WixErrors.MergeLanguageUnsupported(wixMergeRow.SourceLineNumbers, mergeError.Language, wixMergeRow.SourceFile)); + break; + case MsmErrorType.msmErrorResequenceMerge: + Messaging.Instance.OnMessage(WixWarnings.MergeRescheduledAction(wixMergeRow.SourceLineNumbers, mergeError.DatabaseTable, databaseKeys.ToString(), wixMergeRow.SourceFile)); + break; + case MsmErrorType.msmErrorTableMerge: + if ("_Validation" != mergeError.DatabaseTable) // ignore merge errors in the _Validation table + { + Messaging.Instance.OnMessage(WixWarnings.MergeTableFailed(wixMergeRow.SourceLineNumbers, mergeError.DatabaseTable, databaseKeys.ToString(), wixMergeRow.SourceFile)); + } + break; + case MsmErrorType.msmErrorPlatformMismatch: + Messaging.Instance.OnMessage(WixErrors.MergePlatformMismatch(wixMergeRow.SourceLineNumbers, wixMergeRow.SourceFile)); + break; + default: + Messaging.Instance.OnMessage(WixErrors.UnexpectedException(String.Format(CultureInfo.CurrentUICulture, WixStrings.EXP_UnexpectedMergerErrorWithType, Enum.GetName(typeof(MsmErrorType), mergeError.Type), logPath), "InvalidOperationException", Environment.StackTrace)); + break; + } + } + + if (0 >= mergeErrors.Count && !commit) + { + Messaging.Instance.OnMessage(WixErrors.UnexpectedException(String.Format(CultureInfo.CurrentUICulture, WixStrings.EXP_UnexpectedMergerErrorInSourceFile, wixMergeRow.SourceFile, logPath), "InvalidOperationException", Environment.StackTrace)); + } + + if (moduleOpen) + { + merge.CloseModule(); + } + } + } + } + finally + { + if (databaseOpen) + { + merge.CloseDatabase(commit); + } + + if (logOpen) + { + merge.CloseLog(); + } + } + + // stop processing if an error previously occurred + if (Messaging.Instance.EncounteredError) + { + return; + } + + using (Database db = new Database(this.OutputPath, OpenDatabase.Direct)) + { + Table suppressActionTable = this.Output.Tables["WixSuppressAction"]; + + // suppress individual actions + if (null != suppressActionTable) + { + foreach (Row row in suppressActionTable.Rows) + { + if (db.TableExists((string)row[0])) + { + string query = String.Format(CultureInfo.InvariantCulture, "SELECT * FROM {0} WHERE `Action` = '{1}'", row[0].ToString(), (string)row[1]); + + using (View view = db.OpenExecuteView(query)) + { + using (Record record = view.Fetch()) + { + if (null != record) + { + Messaging.Instance.OnMessage(WixWarnings.SuppressMergedAction((string)row[1], row[0].ToString())); + view.Modify(ModifyView.Delete, record); + } + } + } + } + } + } + + // query for merge module actions in suppressed sequences and drop them + foreach (string tableName in this.SuppressedTableNames) + { + if (!db.TableExists(tableName)) + { + continue; + } + + using (View view = db.OpenExecuteView(String.Concat("SELECT `Action` FROM ", tableName))) + { + while (true) + { + using (Record resultRecord = view.Fetch()) + { + if (null == resultRecord) + { + break; + } + + Messaging.Instance.OnMessage(WixWarnings.SuppressMergedAction(resultRecord.GetString(1), tableName)); + } + } + } + + // drop suppressed sequences + using (View view = db.OpenExecuteView(String.Concat("DROP TABLE ", tableName))) + { + } + + // delete the validation rows + using (View view = db.OpenView(String.Concat("DELETE FROM _Validation WHERE `Table` = ?"))) + { + using (Record record = new Record(1)) + { + record.SetString(1, tableName); + view.Execute(record); + } + } + } + + // now update the Attributes column for the files from the Merge Modules + Messaging.Instance.OnMessage(WixVerboses.ResequencingMergeModuleFiles()); + using (View view = db.OpenView("SELECT `Sequence`, `Attributes` FROM `File` WHERE `File`=?")) + { + foreach (FileFacade file in this.FileFacades) + { + if (!file.FromModule) + { + continue; + } + + using (Record record = new Record(1)) + { + record.SetString(1, file.File.File); + view.Execute(record); + } + + using (Record recordUpdate = view.Fetch()) + { + if (null == recordUpdate) + { + throw new InvalidOperationException("Failed to fetch a File row from the database that was merged in from a module."); + } + + recordUpdate.SetInteger(1, file.File.Sequence); + + // update the file attributes to match the compression specified + // on the Merge element or on the Package element + int attributes = 0; + + // get the current value if its not null + if (!recordUpdate.IsNull(2)) + { + attributes = recordUpdate.GetInteger(2); + } + + if (YesNoType.Yes == file.File.Compressed) + { + // these are mutually exclusive + attributes |= MsiInterop.MsidbFileAttributesCompressed; + attributes &= ~MsiInterop.MsidbFileAttributesNoncompressed; + } + else if (YesNoType.No == file.File.Compressed) + { + // these are mutually exclusive + attributes |= MsiInterop.MsidbFileAttributesNoncompressed; + attributes &= ~MsiInterop.MsidbFileAttributesCompressed; + } + else // not specified + { + Debug.Assert(YesNoType.NotSet == file.File.Compressed); + + // clear any compression bits + attributes &= ~MsiInterop.MsidbFileAttributesCompressed; + attributes &= ~MsiInterop.MsidbFileAttributesNoncompressed; + } + + recordUpdate.SetInteger(2, attributes); + + view.Modify(ModifyView.Update, recordUpdate); + } + } + } + + db.Commit(); + } + } + } +} diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/ProcessUncompressedFilesCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/ProcessUncompressedFilesCommand.cs new file mode 100644 index 00000000..b3c09b9e --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Bind/ProcessUncompressedFilesCommand.cs @@ -0,0 +1,118 @@ +// 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.Databases +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.IO; + using WixToolset.Data; + using WixToolset.Data.Rows; + using WixToolset.Msi; + using WixToolset.Core.Native; + using WixToolset.Bind; + using WixToolset.Core.Bind; + using WixToolset.Data.Bind; + + /// + /// Defines the file transfers necessary to layout the uncompressed files. + /// + internal class ProcessUncompressedFilesCommand + { + public string DatabasePath { private get; set; } + + public IEnumerable FileFacades { private get; set; } + + public RowDictionary MediaRows { private get; set; } + + public string LayoutDirectory { private get; set; } + + public bool Compressed { private get; set; } + + public bool LongNamesInImage { private get; set; } + + public Func ResolveMedia { private get; set; } + + public Table WixMediaTable { private get; set; } + + public IEnumerable FileTransfers { get; private set; } + + public void Execute() + { + List fileTransfers = new List(); + + Hashtable directories = new Hashtable(); + + RowDictionary wixMediaRows = new RowDictionary(this.WixMediaTable); + + using (Database db = new Database(this.DatabasePath, OpenDatabase.ReadOnly)) + { + using (View directoryView = db.OpenExecuteView("SELECT `Directory`, `Directory_Parent`, `DefaultDir` FROM `Directory`")) + { + while (true) + { + using (Record directoryRecord = directoryView.Fetch()) + { + if (null == directoryRecord) + { + break; + } + + string sourceName = Common.GetName(directoryRecord.GetString(3), true, this.LongNamesInImage); + + directories.Add(directoryRecord.GetString(1), new ResolvedDirectory(directoryRecord.GetString(2), sourceName)); + } + } + } + + using (View fileView = db.OpenView("SELECT `Directory_`, `FileName` FROM `Component`, `File` WHERE `Component`.`Component`=`File`.`Component_` AND `File`.`File`=?")) + { + using (Record fileQueryRecord = new Record(1)) + { + // for each file in the array of uncompressed files + foreach (FileFacade facade in this.FileFacades) + { + MediaRow mediaRow = this.MediaRows.Get(facade.WixFile.DiskId); + string relativeFileLayoutPath = null; + + WixMediaRow wixMediaRow = null; + string mediaLayoutFolder = null; + + if (wixMediaRows.TryGetValue(mediaRow.GetKey(), out wixMediaRow)) + { + mediaLayoutFolder = wixMediaRow.Layout; + } + + string mediaLayoutDirectory = this.ResolveMedia(mediaRow, mediaLayoutFolder, this.LayoutDirectory); + + // setup up the query record and find the appropriate file in the + // previously executed file view + fileQueryRecord[1] = facade.File.File; + fileView.Execute(fileQueryRecord); + + using (Record fileRecord = fileView.Fetch()) + { + if (null == fileRecord) + { + throw new WixException(WixErrors.FileIdentifierNotFound(facade.File.SourceLineNumbers, facade.File.File)); + } + + relativeFileLayoutPath = Binder.GetFileSourcePath(directories, fileRecord[1], fileRecord[2], this.Compressed, this.LongNamesInImage); + } + + // finally put together the base media layout path and the relative file layout path + string fileLayoutPath = Path.Combine(mediaLayoutDirectory, relativeFileLayoutPath); + FileTransfer transfer; + if (FileTransfer.TryCreate(facade.WixFile.Source, fileLayoutPath, false, "File", facade.File.SourceLineNumbers, out transfer)) + { + fileTransfers.Add(transfer); + } + } + } + } + } + + this.FileTransfers = fileTransfers; + } + } +} diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/UpdateControlTextCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/UpdateControlTextCommand.cs new file mode 100644 index 00000000..7da32206 --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Bind/UpdateControlTextCommand.cs @@ -0,0 +1,80 @@ +// 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.Databases +{ + using System; + using System.IO; + using WixToolset.Data; + using WixToolset.Data.Rows; + + internal class UpdateControlTextCommand + { + public Table BBControlTable { private get; set; } + + public Table WixBBControlTable { private get; set; } + + public Table ControlTable { private get; set; } + + public Table WixControlTable { private get; set; } + + public void Execute() + { + if (null != this.WixBBControlTable) + { + RowDictionary bbControlRows = new RowDictionary(this.BBControlTable); + foreach (Row wixRow in this.WixBBControlTable.Rows) + { + BBControlRow bbControlRow = bbControlRows.Get(wixRow.GetPrimaryKey()); + bbControlRow.Text = this.ReadTextFile(bbControlRow.SourceLineNumbers, wixRow.FieldAsString(2)); + } + } + + if (null != this.WixControlTable) + { + RowDictionary controlRows = new RowDictionary(this.ControlTable); + foreach (Row wixRow in this.WixControlTable.Rows) + { + ControlRow controlRow = controlRows.Get(wixRow.GetPrimaryKey()); + controlRow.Text = this.ReadTextFile(controlRow.SourceLineNumbers, wixRow.FieldAsString(2)); + } + } + } + + /// + /// Reads a text file and returns the contents. + /// + /// Source line numbers for row from source. + /// Source path to file to read. + /// Text string read from file. + private string ReadTextFile(SourceLineNumber sourceLineNumbers, string source) + { + string text = null; + + try + { + using (StreamReader reader = new StreamReader(source)) + { + text = reader.ReadToEnd(); + } + } + catch (DirectoryNotFoundException e) + { + Messaging.Instance.OnMessage(WixErrors.BinderFileManagerMissingFile(sourceLineNumbers, e.Message)); + } + catch (FileNotFoundException e) + { + Messaging.Instance.OnMessage(WixErrors.BinderFileManagerMissingFile(sourceLineNumbers, e.Message)); + } + catch (IOException e) + { + Messaging.Instance.OnMessage(WixErrors.BinderFileManagerMissingFile(sourceLineNumbers, e.Message)); + } + catch (NotSupportedException) + { + Messaging.Instance.OnMessage(WixErrors.FileNotFound(sourceLineNumbers, source)); + } + + return text; + } + } +} diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/UpdateFileFacadesCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/UpdateFileFacadesCommand.cs new file mode 100644 index 00000000..cd9444ee --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Bind/UpdateFileFacadesCommand.cs @@ -0,0 +1,533 @@ +// 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.Databases +{ + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.ComponentModel; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Xml; + using System.Xml.XPath; + using WixToolset.Clr.Interop; + using WixToolset.Core.Bind; + using WixToolset.Data; + using WixToolset.Data.Rows; + using WixToolset.Msi; + + /// + /// Update file information. + /// + internal class UpdateFileFacadesCommand + { + public IEnumerable FileFacades { private get; set; } + + public IEnumerable UpdateFileFacades { private get; set; } + + public string ModularizationGuid { private get; set; } + + public Output Output { private get; set; } + + public bool OverwriteHash { private get; set; } + + public TableDefinitionCollection TableDefinitions { private get; set; } + + public IDictionary VariableCache { private get; set; } + + public void Execute() + { + foreach (FileFacade file in this.UpdateFileFacades) + { + this.UpdateFileFacade(file); + } + } + + private void UpdateFileFacade(FileFacade file) + { + FileInfo fileInfo = null; + try + { + fileInfo = new FileInfo(file.WixFile.Source); + } + catch (ArgumentException) + { + Messaging.Instance.OnMessage(WixDataErrors.InvalidFileName(file.File.SourceLineNumbers, file.WixFile.Source)); + return; + } + catch (PathTooLongException) + { + Messaging.Instance.OnMessage(WixDataErrors.InvalidFileName(file.File.SourceLineNumbers, file.WixFile.Source)); + return; + } + catch (NotSupportedException) + { + Messaging.Instance.OnMessage(WixDataErrors.InvalidFileName(file.File.SourceLineNumbers, file.WixFile.Source)); + return; + } + + if (!fileInfo.Exists) + { + Messaging.Instance.OnMessage(WixErrors.CannotFindFile(file.File.SourceLineNumbers, file.File.File, file.File.FileName, file.WixFile.Source)); + return; + } + + using (FileStream fileStream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + if (Int32.MaxValue < fileStream.Length) + { + throw new WixException(WixErrors.FileTooLarge(file.File.SourceLineNumbers, file.WixFile.Source)); + } + + file.File.FileSize = Convert.ToInt32(fileStream.Length, CultureInfo.InvariantCulture); + } + + string version = null; + string language = null; + try + { + Installer.GetFileVersion(fileInfo.FullName, out version, out language); + } + catch (Win32Exception e) + { + if (0x2 == e.NativeErrorCode) // ERROR_FILE_NOT_FOUND + { + throw new WixException(WixErrors.FileNotFound(file.File.SourceLineNumbers, fileInfo.FullName)); + } + else + { + throw new WixException(WixErrors.Win32Exception(e.NativeErrorCode, e.Message)); + } + } + + // If there is no version, it is assumed there is no language because it won't matter in the versioning of the install. + if (String.IsNullOrEmpty(version)) // unversioned files have their hashes added to the MsiFileHash table + { + if (!this.OverwriteHash) + { + // not overwriting hash, so don't do the rest of these options. + } + else if (null != file.File.Version) + { + // Search all of the file rows available to see if the specified version is actually a companion file. Yes, this looks + // very expensive and you're probably thinking it would be better to create an index of some sort to do an O(1) look up. + // That's a reasonable thought but companion file usage is usually pretty rare so we'd be doing something expensive (indexing + // all the file rows) for a relatively uncommon situation. Let's not do that. + // + // Also, if we do not find a matching file identifier then the user provided a default version and is providing a version + // for unversioned file. That's allowed but generally a dangerous thing to do so let's point that out to the user. + if (!this.FileFacades.Any(r => file.File.Version.Equals(r.File.File, StringComparison.Ordinal))) + { + Messaging.Instance.OnMessage(WixWarnings.DefaultVersionUsedForUnversionedFile(file.File.SourceLineNumbers, file.File.Version, file.File.File)); + } + } + else + { + if (null != file.File.Language) + { + Messaging.Instance.OnMessage(WixWarnings.DefaultLanguageUsedForUnversionedFile(file.File.SourceLineNumbers, file.File.Language, file.File.File)); + } + + int[] hash; + try + { + Installer.GetFileHash(fileInfo.FullName, 0, out hash); + } + catch (Win32Exception e) + { + if (0x2 == e.NativeErrorCode) // ERROR_FILE_NOT_FOUND + { + throw new WixException(WixErrors.FileNotFound(file.File.SourceLineNumbers, fileInfo.FullName)); + } + else + { + throw new WixException(WixErrors.Win32Exception(e.NativeErrorCode, fileInfo.FullName, e.Message)); + } + } + + if (null == file.Hash) + { + Table msiFileHashTable = this.Output.EnsureTable(this.TableDefinitions["MsiFileHash"]); + file.Hash = msiFileHashTable.CreateRow(file.File.SourceLineNumbers); + } + + file.Hash[0] = file.File.File; + file.Hash[1] = 0; + file.Hash[2] = hash[0]; + file.Hash[3] = hash[1]; + file.Hash[4] = hash[2]; + file.Hash[5] = hash[3]; + } + } + else // update the file row with the version and language information. + { + // If no version was provided by the user, use the version from the file itself. + // This is the most common case. + if (String.IsNullOrEmpty(file.File.Version)) + { + file.File.Version = version; + } + else if (!this.FileFacades.Any(r => file.File.Version.Equals(r.File.File, StringComparison.Ordinal))) // this looks expensive, but see explanation below. + { + // The user provided a default version for the file row so we looked for a companion file (a file row with Id matching + // the version value). We didn't find it so, we will override the default version they provided with the actual + // version from the file itself. Now, I know it looks expensive to search through all the file rows trying to match + // on the Id. However, the alternative is to build a big index of all file rows to do look ups. Since this case + // where the file version is already present is rare (companion files are pretty uncommon), we'll do the more + // CPU intensive search to save on the memory intensive index that wouldn't be used much. + // + // Also note this case can occur when the file is being updated using the WixBindUpdatedFiles extension mechanism. + // That's typically even more rare than companion files so again, no index, just search. + file.File.Version = version; + } + + if (!String.IsNullOrEmpty(file.File.Language) && String.IsNullOrEmpty(language)) + { + Messaging.Instance.OnMessage(WixWarnings.DefaultLanguageUsedForVersionedFile(file.File.SourceLineNumbers, file.File.Language, file.File.File)); + } + else // override the default provided by the user (usually nothing) with the actual language from the file itself. + { + file.File.Language = language; + } + + // Populate the binder variables for this file information if requested. + if (null != this.VariableCache) + { + if (!String.IsNullOrEmpty(file.File.Version)) + { + string key = String.Format(CultureInfo.InvariantCulture, "fileversion.{0}", Common.Demodularize(this.Output.Type, this.ModularizationGuid, file.File.File)); + this.VariableCache[key] = file.File.Version; + } + + if (!String.IsNullOrEmpty(file.File.Language)) + { + string key = String.Format(CultureInfo.InvariantCulture, "filelanguage.{0}", Common.Demodularize(this.Output.Type, ModularizationGuid, file.File.File)); + this.VariableCache[key] = file.File.Language; + } + } + } + + // If this is a CLR assembly, load the assembly and get the assembly name information + if (FileAssemblyType.DotNetAssembly == file.WixFile.AssemblyType) + { + bool targetNetfx1 = false; + StringDictionary assemblyNameValues = new StringDictionary(); + + ClrInterop.IReferenceIdentity referenceIdentity = null; + Guid referenceIdentityGuid = ClrInterop.ReferenceIdentityGuid; + uint result = ClrInterop.GetAssemblyIdentityFromFile(fileInfo.FullName, ref referenceIdentityGuid, out referenceIdentity); + if (0 == result && null != referenceIdentity) + { + string imageRuntimeVersion = referenceIdentity.GetAttribute(null, "ImageRuntimeVersion"); + if (null != imageRuntimeVersion) + { + targetNetfx1 = imageRuntimeVersion.StartsWith("v1", StringComparison.OrdinalIgnoreCase); + } + + string culture = referenceIdentity.GetAttribute(null, "Culture") ?? "neutral"; + assemblyNameValues.Add("Culture", culture); + + string name = referenceIdentity.GetAttribute(null, "Name"); + if (null != name) + { + assemblyNameValues.Add("Name", name); + } + + string processorArchitecture = referenceIdentity.GetAttribute(null, "ProcessorArchitecture"); + if (null != processorArchitecture) + { + assemblyNameValues.Add("ProcessorArchitecture", processorArchitecture); + } + + string publicKeyToken = referenceIdentity.GetAttribute(null, "PublicKeyToken"); + if (null != publicKeyToken) + { + bool publicKeyIsNeutral = (String.Equals(publicKeyToken, "neutral", StringComparison.OrdinalIgnoreCase)); + + // Managed code expects "null" instead of "neutral", and + // this won't be installed to the GAC since it's not signed anyway. + assemblyNameValues.Add("publicKeyToken", publicKeyIsNeutral ? "null" : publicKeyToken.ToUpperInvariant()); + assemblyNameValues.Add("publicKeyTokenPreservedCase", publicKeyIsNeutral ? "null" : publicKeyToken); + } + else if (file.WixFile.AssemblyApplication == null) + { + throw new WixException(WixErrors.GacAssemblyNoStrongName(file.File.SourceLineNumbers, fileInfo.FullName, file.File.Component)); + } + + string assemblyVersion = referenceIdentity.GetAttribute(null, "Version"); + if (null != version) + { + assemblyNameValues.Add("Version", assemblyVersion); + } + } + else + { + Messaging.Instance.OnMessage(WixErrors.InvalidAssemblyFile(file.File.SourceLineNumbers, fileInfo.FullName, String.Format(CultureInfo.InvariantCulture, "HRESULT: 0x{0:x8}", result))); + return; + } + + Table assemblyNameTable = this.Output.EnsureTable(this.TableDefinitions["MsiAssemblyName"]); + if (assemblyNameValues.ContainsKey("name")) + { + this.SetMsiAssemblyName(assemblyNameTable, file, "name", assemblyNameValues["name"]); + } + + if (!String.IsNullOrEmpty(version)) + { + this.SetMsiAssemblyName(assemblyNameTable, file, "fileVersion", version); + } + + if (assemblyNameValues.ContainsKey("version")) + { + string assemblyVersion = assemblyNameValues["version"]; + + if (!targetNetfx1) + { + // There is a bug in v1 fusion that requires the assembly's "version" attribute + // to be equal to or longer than the "fileVersion" in length when its present; + // the workaround is to prepend zeroes to the last version number in the assembly + // version. + if (null != version && version.Length > assemblyVersion.Length) + { + string padding = new string('0', version.Length - assemblyVersion.Length); + string[] assemblyVersionNumbers = assemblyVersion.Split('.'); + + if (assemblyVersionNumbers.Length > 0) + { + assemblyVersionNumbers[assemblyVersionNumbers.Length - 1] = String.Concat(padding, assemblyVersionNumbers[assemblyVersionNumbers.Length - 1]); + assemblyVersion = String.Join(".", assemblyVersionNumbers); + } + } + } + + this.SetMsiAssemblyName(assemblyNameTable, file, "version", assemblyVersion); + } + + if (assemblyNameValues.ContainsKey("culture")) + { + this.SetMsiAssemblyName(assemblyNameTable, file, "culture", assemblyNameValues["culture"]); + } + + if (assemblyNameValues.ContainsKey("publicKeyToken")) + { + this.SetMsiAssemblyName(assemblyNameTable, file, "publicKeyToken", assemblyNameValues["publicKeyToken"]); + } + + if (!String.IsNullOrEmpty(file.WixFile.ProcessorArchitecture)) + { + this.SetMsiAssemblyName(assemblyNameTable, file, "processorArchitecture", file.WixFile.ProcessorArchitecture); + } + + if (assemblyNameValues.ContainsKey("processorArchitecture")) + { + this.SetMsiAssemblyName(assemblyNameTable, file, "processorArchitecture", assemblyNameValues["processorArchitecture"]); + } + + // add the assembly name to the information cache + if (null != this.VariableCache) + { + string fileId = Common.Demodularize(this.Output.Type, this.ModularizationGuid, file.File.File); + string key = String.Concat("assemblyfullname.", fileId); + string assemblyName = String.Concat(assemblyNameValues["name"], ", version=", assemblyNameValues["version"], ", culture=", assemblyNameValues["culture"], ", publicKeyToken=", String.IsNullOrEmpty(assemblyNameValues["publicKeyToken"]) ? "null" : assemblyNameValues["publicKeyToken"]); + if (assemblyNameValues.ContainsKey("processorArchitecture")) + { + assemblyName = String.Concat(assemblyName, ", processorArchitecture=", assemblyNameValues["processorArchitecture"]); + } + + this.VariableCache[key] = assemblyName; + + // Add entries with the preserved case publicKeyToken + string pcAssemblyNameKey = String.Concat("assemblyfullnamepreservedcase.", fileId); + this.VariableCache[pcAssemblyNameKey] = (assemblyNameValues["publicKeyToken"] == assemblyNameValues["publicKeyTokenPreservedCase"]) ? assemblyName : assemblyName.Replace(assemblyNameValues["publicKeyToken"], assemblyNameValues["publicKeyTokenPreservedCase"]); + + string pcPublicKeyTokenKey = String.Concat("assemblypublickeytokenpreservedcase.", fileId); + this.VariableCache[pcPublicKeyTokenKey] = assemblyNameValues["publicKeyTokenPreservedCase"]; + } + } + else if (FileAssemblyType.Win32Assembly == file.WixFile.AssemblyType) + { + // TODO: Consider passing in the this.FileFacades as an indexed collection instead of searching through + // all files like this. Even though this is a rare case it looks like we might be able to index the + // file earlier. + FileFacade fileManifest = this.FileFacades.SingleOrDefault(r => r.File.File.Equals(file.WixFile.AssemblyManifest, StringComparison.Ordinal)); + if (null == fileManifest) + { + Messaging.Instance.OnMessage(WixErrors.MissingManifestForWin32Assembly(file.File.SourceLineNumbers, file.File.File, file.WixFile.AssemblyManifest)); + } + + string win32Type = null; + string win32Name = null; + string win32Version = null; + string win32ProcessorArchitecture = null; + string win32PublicKeyToken = null; + + // loading the dom is expensive we want more performant APIs than the DOM + // Navigator is cheaper than dom. Perhaps there is a cheaper API still. + try + { + XPathDocument doc = new XPathDocument(fileManifest.WixFile.Source); + XPathNavigator nav = doc.CreateNavigator(); + nav.MoveToRoot(); + + // this assumes a particular schema for a win32 manifest and does not + // provide error checking if the file does not conform to schema. + // The fallback case here is that nothing is added to the MsiAssemblyName + // table for an out of tolerance Win32 manifest. Perhaps warnings needed. + if (nav.MoveToFirstChild()) + { + while (nav.NodeType != XPathNodeType.Element || nav.Name != "assembly") + { + nav.MoveToNext(); + } + + if (nav.MoveToFirstChild()) + { + bool hasNextSibling = true; + while (nav.NodeType != XPathNodeType.Element || nav.Name != "assemblyIdentity" && hasNextSibling) + { + hasNextSibling = nav.MoveToNext(); + } + if (!hasNextSibling) + { + Messaging.Instance.OnMessage(WixErrors.InvalidManifestContent(file.File.SourceLineNumbers, fileManifest.WixFile.Source)); + return; + } + + if (nav.MoveToAttribute("type", String.Empty)) + { + win32Type = nav.Value; + nav.MoveToParent(); + } + + if (nav.MoveToAttribute("name", String.Empty)) + { + win32Name = nav.Value; + nav.MoveToParent(); + } + + if (nav.MoveToAttribute("version", String.Empty)) + { + win32Version = nav.Value; + nav.MoveToParent(); + } + + if (nav.MoveToAttribute("processorArchitecture", String.Empty)) + { + win32ProcessorArchitecture = nav.Value; + nav.MoveToParent(); + } + + if (nav.MoveToAttribute("publicKeyToken", String.Empty)) + { + win32PublicKeyToken = nav.Value; + nav.MoveToParent(); + } + } + } + } + catch (FileNotFoundException fe) + { + Messaging.Instance.OnMessage(WixErrors.FileNotFound(new SourceLineNumber(fileManifest.WixFile.Source), fe.FileName, "AssemblyManifest")); + } + catch (XmlException xe) + { + Messaging.Instance.OnMessage(WixErrors.InvalidXml(new SourceLineNumber(fileManifest.WixFile.Source), "manifest", xe.Message)); + } + + Table assemblyNameTable = this.Output.EnsureTable(this.TableDefinitions["MsiAssemblyName"]); + if (!String.IsNullOrEmpty(win32Name)) + { + this.SetMsiAssemblyName(assemblyNameTable, file, "name", win32Name); + } + + if (!String.IsNullOrEmpty(win32Version)) + { + this.SetMsiAssemblyName(assemblyNameTable, file, "version", win32Version); + } + + if (!String.IsNullOrEmpty(win32Type)) + { + this.SetMsiAssemblyName(assemblyNameTable, file, "type", win32Type); + } + + if (!String.IsNullOrEmpty(win32ProcessorArchitecture)) + { + this.SetMsiAssemblyName(assemblyNameTable, file, "processorArchitecture", win32ProcessorArchitecture); + } + + if (!String.IsNullOrEmpty(win32PublicKeyToken)) + { + this.SetMsiAssemblyName(assemblyNameTable, file, "publicKeyToken", win32PublicKeyToken); + } + } + } + + /// + /// Set an MsiAssemblyName row. If it was directly authored, override the value, otherwise + /// create a new row. + /// + /// MsiAssemblyName table. + /// FileFacade containing the assembly read for the MsiAssemblyName row. + /// MsiAssemblyName name. + /// MsiAssemblyName value. + private void SetMsiAssemblyName(Table assemblyNameTable, FileFacade file, string name, string value) + { + // check for null value (this can occur when grabbing the file version from an assembly without one) + if (String.IsNullOrEmpty(value)) + { + Messaging.Instance.OnMessage(WixWarnings.NullMsiAssemblyNameValue(file.File.SourceLineNumbers, file.File.Component, name)); + } + else + { + Row assemblyNameRow = null; + + // override directly authored value + foreach (Row row in assemblyNameTable.Rows) + { + if ((string)row[0] == file.File.Component && (string)row[1] == name) + { + assemblyNameRow = row; + break; + } + } + + // if the assembly will be GAC'd and the name in the file table doesn't match the name in the MsiAssemblyName table, error because the install will fail. + if ("name" == name && FileAssemblyType.DotNetAssembly == file.WixFile.AssemblyType && + String.IsNullOrEmpty(file.WixFile.AssemblyApplication) && + !String.Equals(Path.GetFileNameWithoutExtension(file.File.LongFileName), value, StringComparison.OrdinalIgnoreCase)) + { + Messaging.Instance.OnMessage(WixErrors.GACAssemblyIdentityWarning(file.File.SourceLineNumbers, Path.GetFileNameWithoutExtension(file.File.LongFileName), value)); + } + + if (null == assemblyNameRow) + { + assemblyNameRow = assemblyNameTable.CreateRow(file.File.SourceLineNumbers); + assemblyNameRow[0] = file.File.Component; + assemblyNameRow[1] = name; + assemblyNameRow[2] = value; + + // put the MsiAssemblyName row in the same section as the related File row + assemblyNameRow.SectionId = file.File.SectionId; + + if (null == file.AssemblyNames) + { + file.AssemblyNames = new List(); + } + + file.AssemblyNames.Add(assemblyNameRow); + } + else + { + assemblyNameRow[2] = value; + } + + if (this.VariableCache != null) + { + string key = String.Format(CultureInfo.InvariantCulture, "assembly{0}.{1}", name, Common.Demodularize(this.Output.Type, this.ModularizationGuid, file.File.File)).ToLowerInvariant(); + this.VariableCache[key] = (string)assemblyNameRow[2]; + } + } + } + } +} -- cgit v1.2.3-55-g6feb