diff --git a/src/api/wix/WixToolset.Data/ErrorMessages.cs b/src/api/wix/WixToolset.Data/ErrorMessages.cs index 79b835cda..d604e94f9 100644 --- a/src/api/wix/WixToolset.Data/ErrorMessages.cs +++ b/src/api/wix/WixToolset.Data/ErrorMessages.cs @@ -368,6 +368,11 @@ public static Message ExpectedAttribute(SourceLineNumber sourceLineNumbers, stri return Message(sourceLineNumbers, Ids.ExpectedAttribute, "The {0}/@{1} attribute was not found; it is required unless the attribute {2} has a value of '{3}'.", elementName, attributeName, otherAttributeName, otherAttributeValue, otherAttributeValueUnless); } + public static Message ExpectedAttributeInElementOrParent(SourceLineNumber sourceLineNumbers, string elementName, string attributeName) + { + return Message(sourceLineNumbers, Ids.ExpectedAttributeInElementOrParent, "The {0}/@{1} attribute was not found or empty; it is required unless it is specified in the parent element.", elementName, attributeName); + } + public static Message ExpectedAttributeInElementOrParent(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string parentElementName) { return Message(sourceLineNumbers, Ids.ExpectedAttributeInElementOrParent, "The {0}/@{1} attribute was not found or empty; it is required, or it can be specified in the parent {2} element.", elementName, attributeName, parentElementName); diff --git a/src/api/wix/WixToolset.Data/Symbols/HarvestFilesSymbol.cs b/src/api/wix/WixToolset.Data/Symbols/HarvestFilesSymbol.cs new file mode 100644 index 000000000..a3123fc11 --- /dev/null +++ b/src/api/wix/WixToolset.Data/Symbols/HarvestFilesSymbol.cs @@ -0,0 +1,84 @@ +// 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.Data +{ + using WixToolset.Data.Symbols; + + public static partial class SymbolDefinitions + { + public static readonly IntermediateSymbolDefinition HarvestFiles = new IntermediateSymbolDefinition( + SymbolDefinitionType.HarvestFiles, + new[] + { + new IntermediateFieldDefinition(nameof(HarvestFilesSymbolFields.DirectoryRef), IntermediateFieldType.String), + new IntermediateFieldDefinition(nameof(HarvestFilesSymbolFields.Inclusions), IntermediateFieldType.String), + new IntermediateFieldDefinition(nameof(HarvestFilesSymbolFields.Exclusions), IntermediateFieldType.String), + new IntermediateFieldDefinition(nameof(HarvestFilesSymbolFields.ComplexReferenceParentType), IntermediateFieldType.String), + new IntermediateFieldDefinition(nameof(HarvestFilesSymbolFields.ParentId), IntermediateFieldType.String), + new IntermediateFieldDefinition(nameof(HarvestFilesSymbolFields.SourcePath), IntermediateFieldType.String), + }, + typeof(HarvestFilesSymbol)); + } +} + +namespace WixToolset.Data.Symbols +{ + public enum HarvestFilesSymbolFields + { + DirectoryRef, + Inclusions, + Exclusions, + ComplexReferenceParentType, + ParentId, + SourcePath, + } + + public class HarvestFilesSymbol : IntermediateSymbol + { + public HarvestFilesSymbol() : base(SymbolDefinitions.HarvestFiles, null, null) + { + } + + public HarvestFilesSymbol(SourceLineNumber sourceLineNumber, Identifier id = null) : base(SymbolDefinitions.HarvestFiles, sourceLineNumber, id) + { + } + + public IntermediateField this[HarvestFilesSymbolFields index] => this.Fields[(int)index]; + + public string DirectoryRef + { + get => (string)this.Fields[(int)HarvestFilesSymbolFields.DirectoryRef]; + set => this.Set((int)HarvestFilesSymbolFields.DirectoryRef, value); + } + + public string Inclusions + { + get => (string)this.Fields[(int)HarvestFilesSymbolFields.Inclusions]; + set => this.Set((int)HarvestFilesSymbolFields.Inclusions, value); + } + + public string Exclusions + { + get => (string)this.Fields[(int)HarvestFilesSymbolFields.Exclusions]; + set => this.Set((int)HarvestFilesSymbolFields.Exclusions, value); + } + + public string ComplexReferenceParentType + { + get => (string)this.Fields[(int)HarvestFilesSymbolFields.ComplexReferenceParentType]; + set => this.Set((int)HarvestFilesSymbolFields.ComplexReferenceParentType, value); + } + + public string ParentId + { + get => (string)this.Fields[(int)HarvestFilesSymbolFields.ParentId]; + set => this.Set((int)HarvestFilesSymbolFields.ParentId, value); + } + + public string SourcePath + { + get => (string)this.Fields[(int)HarvestFilesSymbolFields.SourcePath]; + set => this.Set((int)HarvestFilesSymbolFields.SourcePath, value); + } + } +} diff --git a/src/api/wix/WixToolset.Data/Symbols/SymbolDefinitions.cs b/src/api/wix/WixToolset.Data/Symbols/SymbolDefinitions.cs index 3b545a71d..67c004313 100644 --- a/src/api/wix/WixToolset.Data/Symbols/SymbolDefinitions.cs +++ b/src/api/wix/WixToolset.Data/Symbols/SymbolDefinitions.cs @@ -40,6 +40,7 @@ public enum SymbolDefinitionType FeatureComponents, File, FileSFPCatalog, + HarvestFiles, Icon, ImageFamilies, IniFile, diff --git a/src/api/wix/WixToolset.Extensibility/Services/IParseHelper.cs b/src/api/wix/WixToolset.Extensibility/Services/IParseHelper.cs index 3c20c14b9..e7856c7c5 100644 --- a/src/api/wix/WixToolset.Extensibility/Services/IParseHelper.cs +++ b/src/api/wix/WixToolset.Extensibility/Services/IParseHelper.cs @@ -11,7 +11,7 @@ namespace WixToolset.Extensibility.Services using WixToolset.Extensibility.Data; /// - /// Interface provided to help compiler extensions parse. + /// Interface provided to help compiler and optimizer extensions parse. /// public interface IParseHelper { diff --git a/src/wix/WixToolset.Core/CommandLine/BuildCommand.cs b/src/wix/WixToolset.Core/CommandLine/BuildCommand.cs index b0cd174ac..cc0de13a1 100644 --- a/src/wix/WixToolset.Core/CommandLine/BuildCommand.cs +++ b/src/wix/WixToolset.Core/CommandLine/BuildCommand.cs @@ -125,12 +125,8 @@ public override Task ExecuteAsync(CancellationToken cancellationToken) { using (new IntermediateFieldContext("wix.link")) { - var wixipl = inputsOutputs.Wixipls.SingleOrDefault(); - - if (wixipl == null) - { - wixipl = this.LinkPhase(wixobjs, inputsOutputs, creator, cancellationToken); - } + var wixipl = inputsOutputs.Wixipls.SingleOrDefault() + ?? this.LinkPhase(wixobjs, inputsOutputs, creator, cancellationToken); if (!this.Messaging.EncounteredError) { diff --git a/src/wix/WixToolset.Core/Compiler.cs b/src/wix/WixToolset.Core/Compiler.cs index bafe2c195..a98d45740 100644 --- a/src/wix/WixToolset.Core/Compiler.cs +++ b/src/wix/WixToolset.Core/Compiler.cs @@ -2655,6 +2655,9 @@ private void ParseComponentGroupElement(XElement node, ComplexReferenceParentTyp case "File": this.ParseNakedFileElement(child, ComplexReferenceParentType.ComponentGroup, id.Id, directoryId, source); break; + case "Files": + this.ParseFilesElement(child, ComplexReferenceParentType.ComponentGroup, id.Id, directoryId, source); + break; default: this.Core.UnexpectedElement(node, child); break; @@ -3106,7 +3109,7 @@ private void ParseCopyFileElement(XElement node, string componentId, string file this.Core.AddSymbol(new MoveFileSymbol(sourceLineNumbers, id) { ComponentRef = componentId, - SourceName = sourceName, + SourceName = sourceName, DestinationName = destinationName, DestinationShortName = destinationShortName, SourceFolder = sourceDirectory ?? sourceProperty, @@ -3881,6 +3884,9 @@ private void ParseDirectoryElement(XElement node, string parentId, int diskId, s case "File": this.ParseNakedFileElement(child, ComplexReferenceParentType.Unknown, null, id.Id, fileSource); break; + case "Files": + this.ParseFilesElement(child, ComplexReferenceParentType.Unknown, null, id.Id, fileSource); + break; case "Merge": this.ParseMergeElement(child, id.Id, diskId); break; @@ -3996,6 +4002,9 @@ private void ParseDirectoryRefElement(XElement node) case "File": this.ParseNakedFileElement(child, ComplexReferenceParentType.Unknown, null, id, fileSource); break; + case "Files": + this.ParseFilesElement(child, ComplexReferenceParentType.Unknown, null, id, fileSource); + break; case "Merge": this.ParseMergeElement(child, id, diskId); break; @@ -4436,6 +4445,9 @@ private void ParseFeatureElement(XElement node, ComplexReferenceParentType paren case "File": this.ParseNakedFileElement(child, ComplexReferenceParentType.Feature, id.Id, null, null); break; + case "Files": + this.ParseFilesElement(child, ComplexReferenceParentType.Feature, id.Id, null, null); + break; case "Level": this.ParseLevelElement(child, id.Id); break; @@ -4579,6 +4591,9 @@ private void ParseFeatureRefElement(XElement node, ComplexReferenceParentType pa case "File": this.ParseNakedFileElement(child, ComplexReferenceParentType.Feature, id, null, null); break; + case "Files": + this.ParseFilesElement(child, ComplexReferenceParentType.Feature, id, null, null); + break; case "MergeRef": this.ParseMergeRefElement(child, ComplexReferenceParentType.Feature, id); break; @@ -4667,6 +4682,9 @@ private void ParseFeatureGroupElement(XElement node, ComplexReferenceParentType case "File": this.ParseNakedFileElement(child, ComplexReferenceParentType.FeatureGroup, id.Id, null, null); break; + case "Files": + this.ParseFilesElement(child, ComplexReferenceParentType.Feature, id.Id, null, null); + break; case "MergeRef": this.ParseMergeRefElement(child, ComplexReferenceParentType.FeatureGroup, id.Id); break; @@ -5677,6 +5695,129 @@ private void ParseNakedFileElement(XElement node, ComplexReferenceParentType par } } + /// + /// Parses a `Files` element. + /// + /// Files element to parse. + /// Type of complex reference parent. Will be Unknown if there is no parent. + /// Optional identifier for primary parent. + /// Ancestor's directory id. + /// Default source path of parent directory. + private void ParseFilesElement(XElement node, ComplexReferenceParentType parentType, string parentId, string directoryId, string sourcePath) + { + var sourceLineNumbers = Preprocessor.GetSourceLineNumbers(node); + var win64 = this.Context.IsCurrentPlatform64Bit; + string subdirectory = null; + var inclusions = new List(); + var exclusions = new List(); + + foreach (var attrib in node.Attributes()) + { + if (String.IsNullOrEmpty(attrib.Name.NamespaceName) || CompilerCore.WixNamespace == attrib.Name.Namespace) + { + switch (attrib.Name.LocalName) + { + case "Directory": + directoryId = this.Core.GetAttributeIdentifierValue(sourceLineNumbers, attrib); + this.Core.CreateSimpleReference(sourceLineNumbers, SymbolDefinitions.Directory, directoryId); + break; + case "Subdirectory": + subdirectory = this.Core.GetAttributeLongFilename(sourceLineNumbers, attrib, allowRelative: true); + break; + case "Include": + inclusions.AddRange(this.Core.GetAttributeValue(sourceLineNumbers, attrib).Split(';')); + break; + default: + this.Core.UnexpectedAttribute(node, attrib); + break; + } + } + else + { + var context = new Dictionary() { { "Win64", win64.ToString() } }; + this.Core.ParseExtensionAttribute(node, attrib, context); + } + } + + foreach (var child in node.Elements()) + { + if (CompilerCore.WixNamespace == child.Name.Namespace) + { + switch (child.Name.LocalName) + { + case "Exclude": + this.ParseFilesExcludeElement(child, exclusions); + break; + default: + this.Core.UnexpectedElement(node, child); + break; + } + } + else + { + var context = new Dictionary() { { "Win64", win64.ToString() } }; + this.Core.ParseExtensionElement(node, child, context); + } + } + + if (String.IsNullOrEmpty(directoryId)) + { + directoryId = "INSTALLFOLDER"; + this.Core.CreateSimpleReference(sourceLineNumbers, SymbolDefinitions.Directory, directoryId); + } + else if (!String.IsNullOrEmpty(subdirectory)) + { + directoryId = this.HandleSubdirectory(sourceLineNumbers, node, directoryId, subdirectory, "Directory", "Subdirectory"); + } + + if (!inclusions.Any()) + { + this.Core.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, node.Name.LocalName, "Include")); + } + + var inclusionsAsString = String.Join(";", inclusions); + var exclusionsAsString = String.Join(";", exclusions); + + var id = this.Core.CreateIdentifier("hvf", directoryId, inclusionsAsString, exclusionsAsString); + + this.Core.AddSymbol(new HarvestFilesSymbol(sourceLineNumbers, id) + { + DirectoryRef = directoryId, + Inclusions = inclusionsAsString, + Exclusions = exclusionsAsString, + ComplexReferenceParentType = parentType.ToString(), + ParentId = parentId, + SourcePath = sourcePath, + }); + } + + private void ParseFilesExcludeElement(XElement node, IList paths) + { + var sourceLineNumbers = Preprocessor.GetSourceLineNumbers(node); + + foreach (var attrib in node.Attributes()) + { + if (String.IsNullOrEmpty(attrib.Name.NamespaceName) || CompilerCore.WixNamespace == attrib.Name.Namespace) + { + switch (attrib.Name.LocalName) + { + case "Files": + paths.Add(this.Core.GetAttributeValue(sourceLineNumbers, attrib)); + break; + default: + this.Core.UnexpectedAttribute(node, attrib); + break; + } + } + else + { + this.Core.ParseExtensionAttribute(node, attrib); + } + } + + this.Core.ParseForExtensionElements(node); + } + /// /// Parses a file search element. /// @@ -5997,6 +6138,9 @@ private void ParseFragmentElement(XElement node) case "File": this.ParseNakedFileElement(child, ComplexReferenceParentType.Unknown, null, null, null); break; + case "Files": + this.ParseFilesElement(child, ComplexReferenceParentType.Unknown, null, null, null); + break; case "Icon": this.ParseIconElement(child); break; @@ -7347,6 +7491,9 @@ private void ParseStandardDirectoryElement(XElement node) case "File": this.ParseNakedFileElement(child, ComplexReferenceParentType.Unknown, null, id, null); break; + case "Files": + this.ParseFilesElement(child, ComplexReferenceParentType.Unknown, null, id, null); + break; case "Merge": this.ParseMergeElement(child, id, diskId: CompilerConstants.IntegerNotSet); break; diff --git a/src/wix/WixToolset.Core/Compiler_Module.cs b/src/wix/WixToolset.Core/Compiler_Module.cs index 19f57773f..08f47657f 100644 --- a/src/wix/WixToolset.Core/Compiler_Module.cs +++ b/src/wix/WixToolset.Core/Compiler_Module.cs @@ -178,6 +178,9 @@ private void ParseModuleElement(XElement node) case "File": this.ParseNakedFileElement(child, ComplexReferenceParentType.Module, this.activeName, null, null); break; + case "Files": + this.ParseFilesElement(child, ComplexReferenceParentType.Module, this.activeName, null, null); + break; case "Icon": this.ParseIconElement(child); break; diff --git a/src/wix/WixToolset.Core/Compiler_Package.cs b/src/wix/WixToolset.Core/Compiler_Package.cs index 220a2a761..8856930a2 100644 --- a/src/wix/WixToolset.Core/Compiler_Package.cs +++ b/src/wix/WixToolset.Core/Compiler_Package.cs @@ -306,6 +306,9 @@ private void ParsePackageElement(XElement node) case "File": this.ParseNakedFileElement(child, ComplexReferenceParentType.Product, productCode, null, null); break; + case "Files": + this.ParseFilesElement(child, ComplexReferenceParentType.Unknown, null, null, null); + break; case "Icon": this.ParseIconElement(child); break; diff --git a/src/wix/WixToolset.Core/HarvestFilesCommand.cs b/src/wix/WixToolset.Core/HarvestFilesCommand.cs new file mode 100644 index 000000000..c85abf7f6 --- /dev/null +++ b/src/wix/WixToolset.Core/HarvestFilesCommand.cs @@ -0,0 +1,257 @@ +// 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 +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using WixToolset.Data; + using WixToolset.Data.Symbols; + using WixToolset.Extensibility.Data; + using WixToolset.Extensibility.Services; + + internal class HarvestFilesCommand + { + private const string BindPathOpenString = "!(bindpath."; + + public HarvestFilesCommand(IOptimizeContext context) + { + this.Context = context; + this.Messaging = this.Context.ServiceProvider.GetService(); + this.ParseHelper = this.Context.ServiceProvider.GetService(); + } + + public IOptimizeContext Context { get; } + + public IMessaging Messaging { get; } + + public IParseHelper ParseHelper { get; } + + internal void Execute() + { + var harvestedFiles = new HashSet(); + + foreach (var section in this.Context.Intermediates.SelectMany(i => i.Sections)) + { + foreach (var harvestFiles in section.Symbols.OfType().ToList()) + { + this.HarvestFiles(harvestFiles, section, harvestedFiles); + + section.RemoveSymbol(harvestFiles); + } + } + } + + private void HarvestFiles(HarvestFilesSymbol harvestFile, IntermediateSection section, ISet harvestedFiles) + { + var unusedSectionCachedInlinedDirectoryIds = new Dictionary(); + + var directoryId = harvestFile.DirectoryRef; + + var inclusions = harvestFile.Inclusions.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + var exclusions = harvestFile.Exclusions.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + + var comparer = new WildcardFileComparer(); + + var resolvedFiles = Enumerable.Empty(); + + try + { + var included = this.GetWildcardFiles(harvestFile, inclusions); + var excluded = this.GetWildcardFiles(harvestFile, exclusions); + + resolvedFiles = included.Except(excluded, comparer).ToList(); + + if (!resolvedFiles.Any()) + { + this.Messaging.Write(OptimizerWarnings.ZeroFilesHarvested(harvestFile.SourceLineNumbers)); + } + } + catch (DirectoryNotFoundException e) + { + this.Messaging.Write(OptimizerWarnings.ExpectedDirectory(harvestFile.SourceLineNumbers, e.Message)); + + return; + } + + foreach (var fileByRecursiveDir in resolvedFiles.GroupBy(resolvedFile => resolvedFile.RecursiveDir, resolvedFile => resolvedFile.Path)) + { + var recursiveDir = fileByRecursiveDir.Key; + + if (!String.IsNullOrEmpty(recursiveDir)) + { + directoryId = this.ParseHelper.CreateDirectoryReferenceFromInlineSyntax(section, harvestFile.SourceLineNumbers, attribute: null, directoryId, recursiveDir, unusedSectionCachedInlinedDirectoryIds); + } + + foreach (var file in fileByRecursiveDir) + { + if (harvestedFiles.Add(file)) + { + var name = Path.GetFileName(file); + + var id = this.ParseHelper.CreateIdentifier("fls", directoryId, name); + + section.AddSymbol(new FileSymbol(harvestFile.SourceLineNumbers, id) + { + ComponentRef = id.Id, + Name = name, + Attributes = FileSymbolAttributes.None | FileSymbolAttributes.Vital, + DirectoryRef = directoryId, + Source = new IntermediateFieldPathValue { Path = file }, + }); + + section.AddSymbol(new ComponentSymbol(harvestFile.SourceLineNumbers, id) + { + ComponentId = "*", + DirectoryRef = directoryId, + Location = ComponentLocation.LocalOnly, + KeyPath = id.Id, + KeyPathType = ComponentKeyPathType.File, + DisableRegistryReflection = false, + NeverOverwrite = false, + Permanent = false, + SharedDllRefCount = false, + Shared = false, + Transitive = false, + UninstallWhenSuperseded = false, + Win64 = this.Context.Platform == Platform.ARM64 || this.Context.Platform == Platform.X64, + }); + + if (Enum.TryParse(harvestFile.ComplexReferenceParentType, out var parentType) + && ComplexReferenceParentType.Unknown != parentType && null != harvestFile.ParentId) + { + // If the parent was provided, add a complex reference to that, and, if + // the Files is under a feature, then mark the complex reference primary. + this.ParseHelper.CreateComplexReference(section, harvestFile.SourceLineNumbers, parentType, harvestFile.ParentId, null, ComplexReferenceChildType.Component, id.Id, ComplexReferenceParentType.Feature == parentType); + } + } + else + { + this.Messaging.Write(OptimizerWarnings.SkippingDuplicateFile(harvestFile.SourceLineNumbers, file)); + } + } + } + } + + private IEnumerable GetWildcardFiles(HarvestFilesSymbol harvestFile, IEnumerable patterns) + { + var sourceLineNumbers = harvestFile.SourceLineNumbers; + var sourcePath = harvestFile.SourcePath; + + var files = new List(); + + foreach (var pattern in patterns) + { + // Resolve bind paths, if any, which might result in multiple directories. + foreach (var path in this.ResolveBindPaths(sourceLineNumbers, pattern)) + { + var sourceDirectory = String.IsNullOrEmpty(sourcePath) ? Path.GetDirectoryName(sourceLineNumbers.FileName) : sourcePath; + var recursive = path.IndexOf("**") >= 0; + var filePortion = Path.GetFileName(path); + var directoryPortion = Path.GetDirectoryName(path); + + if (directoryPortion?.EndsWith(@"\**") == true) + { + directoryPortion = directoryPortion.Substring(0, directoryPortion.Length - 3); + } + + var recursiveDirOffset = directoryPortion.Length + 1; + + if (directoryPortion is null || directoryPortion.Length == 0 || directoryPortion == "**") + { + directoryPortion = sourceDirectory; + recursiveDirOffset = sourceDirectory.Length + 1; + + } + else if (!Path.IsPathRooted(directoryPortion)) + { + directoryPortion = Path.Combine(sourceDirectory, directoryPortion); + recursiveDirOffset = sourceDirectory.Length + 1; + } + + var foundFiles = Directory.EnumerateFiles(directoryPortion, filePortion, recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); + + foreach (var foundFile in foundFiles) + { + var recursiveDir = Path.GetDirectoryName(foundFile.Substring(recursiveDirOffset)); + files.Add(new WildcardFile() + { + RecursiveDir = recursiveDir, + Path = foundFile, + }); + } + } + } + + return files; + } + + private IEnumerable ResolveBindPaths(SourceLineNumber sourceLineNumbers, string source) + { + var resultingDirectories = new List(); + + var bindName = String.Empty; + var path = source; + + if (source.StartsWith(BindPathOpenString, StringComparison.Ordinal)) + { + var closeParen = source.IndexOf(')', BindPathOpenString.Length); + + if (-1 != closeParen) + { + bindName = source.Substring(BindPathOpenString.Length, closeParen - BindPathOpenString.Length); + path = source.Substring(BindPathOpenString.Length + bindName.Length + 1); // +1 for the closing brace. + path = path.TrimStart('\\'); // remove starting '\\' char so the path doesn't look rooted. + } + } + + if (String.IsNullOrEmpty(bindName)) + { + resultingDirectories.Add(path); + } + else + { + var foundBindPath = false; + + foreach (var bindPath in this.Context.BindPaths) + { + if (bindName.Equals(bindPath.Name, StringComparison.OrdinalIgnoreCase)) + { + var resolved = Path.Combine(bindPath.Path, path); + resultingDirectories.Add(resolved); + + foundBindPath = true; + } + } + + if (!foundBindPath) + { + this.Messaging.Write(OptimizerWarnings.ExpectedDirectory(sourceLineNumbers, source)); + } + } + + return resultingDirectories; + } + + private class WildcardFile + { + public string RecursiveDir { get; set; } + + public string Path { get; set; } + } + + private class WildcardFileComparer : IEqualityComparer + { + public bool Equals(WildcardFile x, WildcardFile y) + { + return x?.Path == y?.Path; + } + + public int GetHashCode(WildcardFile obj) + { + return obj?.Path?.GetHashCode() ?? 0; + } + } + } +} diff --git a/src/wix/WixToolset.Core/Optimizer.cs b/src/wix/WixToolset.Core/Optimizer.cs index 5864121ee..33f757a31 100644 --- a/src/wix/WixToolset.Core/Optimizer.cs +++ b/src/wix/WixToolset.Core/Optimizer.cs @@ -25,7 +25,10 @@ public void Optimize(IOptimizeContext context) extension.PreOptimize(context); } - // TODO: Fill with useful optimization features. + { + var command = new HarvestFilesCommand(context); + command.Execute(); + } foreach (var extension in context.Extensions) { diff --git a/src/wix/WixToolset.Core/OptimizerWarnings.cs b/src/wix/WixToolset.Core/OptimizerWarnings.cs new file mode 100644 index 000000000..784dc587c --- /dev/null +++ b/src/wix/WixToolset.Core/OptimizerWarnings.cs @@ -0,0 +1,36 @@ +// 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 +{ + using WixToolset.Data; + + internal static class OptimizerWarnings + { + public static Message ZeroFilesHarvested(SourceLineNumber sourceLineNumbers) + { + return Message(sourceLineNumbers, Ids.ZeroFilesHarvested, "Files inclusions and exclusions resulted in zero files harvested. Unless that is expected, you should verify your Files paths, inclusions, and exclusions for accuracy."); + } + + public static Message ExpectedDirectory(SourceLineNumber sourceLineNumbers, string harvestDirectory) + { + return Message(sourceLineNumbers, Ids.ExpectedDirectory, "Missing directory for harvesting files: {0}", harvestDirectory); + } + + public static Message SkippingDuplicateFile(SourceLineNumber sourceLineNumbers, string duplicateFile) + { + return Message(sourceLineNumbers, Ids.SkippingDuplicateFile, "Skipping file that has already been harvested: {0}", duplicateFile); + } + + private static Message Message(SourceLineNumber sourceLineNumber, Ids id, string format, params object[] args) + { + return new Message(sourceLineNumber, MessageLevel.Warning, (int)id, format, args); + } + + public enum Ids + { + ZeroFilesHarvested = 8600, + ExpectedDirectory = 8601, + SkippingDuplicateFile = 8602, + } + } +} diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/HarvestFilesFixture.cs b/src/wix/test/WixToolsetTest.CoreIntegration/HarvestFilesFixture.cs new file mode 100644 index 000000000..1aa0f5e75 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/HarvestFilesFixture.cs @@ -0,0 +1,319 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolsetTest.CoreIntegration +{ + using System.Data; + using System.IO; + using System.Linq; + using WixInternal.Core.TestPackage; + using WixInternal.TestSupport; + using Xunit; + + public class HarvestFilesFixture + { + [Fact] + public void MustIncludeSomeFiles() + { + var messages = BuildAndQueryComponentAndFileTables("BadAuthoring.wxs", isPackage: true, 10); + Assert.Equal(new[] + { + "10", + }, messages); + } + + [Fact] + public void ZeroFilesHarvestedIsAWarning() + { + var messages = BuildAndQueryComponentAndFileTables("ZeroFiles.wxs", isPackage: true, 8600); + Assert.Equal(new[] + { + "8600", + }, messages); + } + + [Fact] + public void MissingHarvestDirectoryIsAWarning() + { + var messages = BuildAndQueryComponentAndFileTables("BadDirectory.wxs", isPackage: true, 8601); + Assert.Equal(new[] + { + "8601", + "8601", + }, messages); + } + + [Fact] + public void DuplicateFilesSomethingSomething() + { + var messages = BuildAndQueryComponentAndFileTables("DuplicateFiles.wxs", isPackage: true, 8602); + Assert.Equal(new[] + { + "8602", + "8602", + "8602", + "8602", + }, messages); + } + + [Fact] + public void CanHarvestFilesInComponentGroup() + { + var rows = BuildAndQueryComponentAndFileTables("ComponentGroup.wxs"); + + var fileNames = AssertFileComponentIds(4, rows); + Assert.Equal(new[] + { + "test20.txt", + "test21.txt", + "test3.txt", + "test4.txt", + }, fileNames); + } + + [Fact] + public void CanHarvestFilesInDirectory() + { + var rows = BuildAndQueryComponentAndFileTables("Directory.wxs"); + + var fileNames = AssertFileComponentIds(5, rows); + Assert.Equal(new[] + { + "test10.txt", + "test120.txt", + "test2.txt", + "test3.txt", + "test4.txt", + }, fileNames); + } + + [Fact] + public void CanHarvestFilesInDirectoryRef() + { + var rows = BuildAndQueryComponentAndFileTables("DirectoryRef.wxs"); + + var fileNames = AssertFileComponentIds(9, rows); + Assert.Equal(new[] + { + "notatest.txt", + "pleasedontincludeme.dat", + "test1.txt", + "test120.txt", + "test2.txt", + "test20.txt", + "test21.txt", + "test3.txt", + "test4.txt", + }, fileNames); + } + + [Fact] + public void CanHarvestFilesInFeature() + { + var rows = BuildAndQueryComponentAndFileTables("Feature.wxs"); + + AssertFileComponentIds(3, rows); + } + + [Fact] + public void CanHarvestFilesInFeatureGroup() + { + var rows = BuildAndQueryComponentAndFileTables("FeatureGroup.wxs"); + + var fileNames = AssertFileComponentIds(8, rows); + Assert.Equal(new[] + { + "notatest.txt", + "pleasedontincludeme.dat", + "test1.txt", + "test2.txt", + "test20.txt", + "test21.txt", + "test3.txt", + "test4.txt", + }, fileNames); + } + + [Fact] + public void CanHarvestFilesInFeatureRef() + { + var rows = BuildAndQueryComponentAndFileTables("FeatureRef.wxs"); + + var fileNames = AssertFileComponentIds(8, rows); + Assert.Equal(new[] + { + "notatest.txt", + "pleasedontincludeme.dat", + "test1.txt", + "test2.txt", + "test20.txt", + "test21.txt", + "test3.txt", + "test4.txt", + }, fileNames); + } + + [Fact] + public void CanHarvestFilesInFragments() + { + var rows = BuildAndQueryComponentAndFileTables("Fragment.wxs"); + + var fileNames = AssertFileComponentIds(5, rows); + Assert.Equal(new[] + { + "notatest.txt", + "test1.txt", + "test2.txt", + "test3.txt", + "test4.txt", + }, fileNames); + } + + [Fact] + public void CanHarvestFilesInModules() + { + var rows = BuildAndQueryComponentAndFileTables("Module.wxs", isPackage: false); + + var fileNames = AssertFileComponentIds(5, rows); + Assert.Equal(new[] + { + "notatest.txt", + "test1.txt", + "test2.txt", + "test3.txt", + "test4.txt", + }, fileNames); + } + + [Fact] + public void CanHarvestFilesWithBindPaths() + { + var rows = BuildAndQueryComponentAndFileTables("BindPaths.wxs"); + + var fileNames = AssertFileComponentIds(8, rows); + Assert.Equal(new[] + { + "test1.txt", + "test10.txt", + "test120.txt", + "test2.txt", + "test20.txt", + "test21.txt", + "test3.txt", + "test4.txt", + }, fileNames); + } + + [Fact] + public void HarvestedFilesUnderPackageWithAuthoredFeatureAreOrphaned() + { + var messages = BuildAndQueryComponentAndFileTables("PackageWithoutDefaultFeature.wxs", isPackage: true, 267); + Assert.Equal(new[] + { + "267", + "267", + "267", + "267", + }, messages); + } + + [Fact] + public void CanHarvestFilesInStandardDirectory() + { + var rows = BuildAndQueryComponentAndFileTables("StandardDirectory.wxs"); + + var fileNames = AssertFileComponentIds(10, rows); + Assert.Equal(new[] + { + "notatest.txt", + "pleasedontincludeme.dat", + "test1.txt", + "test10.txt", + "test120.txt", + "test2.txt", + "test20.txt", + "test21.txt", + "test3.txt", + "test4.txt", + }, fileNames); + } + + private static string[] BuildAndQueryComponentAndFileTables(string file, bool isPackage = true, int? exitCode = null) + { + var folder = TestData.Get("TestData", "HarvestFiles"); + + using (var fs = new DisposableFileSystem()) + { + var baseFolder = fs.GetFolder(); + var intermediateFolder = Path.Combine(baseFolder, "obj"); + var binFolder = Path.Combine(baseFolder, "bin"); + var msiPath = Path.Combine(binFolder, isPackage ? "test.msi" : "test.msm"); + + var arguments = new[] + { + "build", + Path.Combine(folder, file), + "-intermediateFolder", intermediateFolder, + "-bindpath", folder, + "-bindpath", @$"ToBeHarvested={folder}\files1", + "-bindpath", @$"ToBeHarvested={folder}\files2", + "-o", msiPath, + }; + + var result = WixRunner.Execute(arguments); + + if (exitCode.HasValue) + { + Assert.Equal(exitCode.Value, result.ExitCode); + + return result.Messages.Select(m => m.Id.ToString()).ToArray(); + } + else + { + result.AssertSuccess(); + + return Query.QueryDatabase(msiPath, new[] { "Component", "File" }) + .OrderBy(s => s) + .ToArray(); + } + } + } + + private static string[] AssertFileComponentIds(int fileCount, string[] rows) + { + var componentRows = rows.Where(row => row.StartsWith("Component:")).ToArray(); + var fileRows = rows.Where(row => row.StartsWith("File:")).ToArray(); + + Assert.Equal(fileCount, componentRows.Length); + Assert.Equal(componentRows.Length, fileRows.Length); + + // Component id == Component keypath == File id + foreach (var componentRow in componentRows) + { + var columns = componentRow.Split(':', '\t'); + Assert.Equal(columns[1], columns[6]); + } + + foreach (var fileRow in fileRows) + { + var columns = fileRow.Split(':', '\t'); + Assert.Equal(columns[1], columns[2]); + } + + + var files = fileRows.Select(row => row.Split('\t')[2]); + + var lfns = files.Select(name => name.Split('|')); + + return fileRows + .Select(row => row.Split('\t')[2]) + .Select(GetLFN) + .OrderBy(name => name).ToArray(); + + static string GetLFN(string possibleSfnLfnPair) + { + var parts = possibleSfnLfnPair.Split('|'); + return parts[parts.Length - 1]; + } + } + } +} diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/BadAuthoring.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/BadAuthoring.wxs new file mode 100644 index 000000000..c8f25e1d4 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/BadAuthoring.wxs @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/BadDirectory.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/BadDirectory.wxs new file mode 100644 index 000000000..88cbb3eba --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/BadDirectory.wxs @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/BindPaths.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/BindPaths.wxs new file mode 100644 index 000000000..b0495125f --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/BindPaths.wxs @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/ComponentGroup.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/ComponentGroup.wxs new file mode 100644 index 000000000..963f40a53 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/ComponentGroup.wxs @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/Directory.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/Directory.wxs new file mode 100644 index 000000000..4a2a91a44 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/Directory.wxs @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/DirectoryRef.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/DirectoryRef.wxs new file mode 100644 index 000000000..4eed3106b --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/DirectoryRef.wxs @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/DuplicateFiles.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/DuplicateFiles.wxs new file mode 100644 index 000000000..8375e0ae1 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/DuplicateFiles.wxs @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/Feature.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/Feature.wxs new file mode 100644 index 000000000..05152d722 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/Feature.wxs @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/FeatureGroup.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/FeatureGroup.wxs new file mode 100644 index 000000000..5c1b2165c --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/FeatureGroup.wxs @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/FeatureRef.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/FeatureRef.wxs new file mode 100644 index 000000000..86a90e292 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/FeatureRef.wxs @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/Fragment.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/Fragment.wxs new file mode 100644 index 000000000..6f5053d25 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/Fragment.wxs @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/Module.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/Module.wxs new file mode 100644 index 000000000..7034aa9d1 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/Module.wxs @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/PackageWithoutDefaultFeature.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/PackageWithoutDefaultFeature.wxs new file mode 100644 index 000000000..3157202a7 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/PackageWithoutDefaultFeature.wxs @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/StandardDirectory.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/StandardDirectory.wxs new file mode 100644 index 000000000..1838ed66b --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/StandardDirectory.wxs @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/ZeroFiles.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/ZeroFiles.wxs new file mode 100644 index 000000000..e733622f3 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/ZeroFiles.wxs @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files1/files1_sub1/files1_sub2/test120.txt b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files1/files1_sub1/files1_sub2/test120.txt new file mode 100644 index 000000000..d32727e04 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files1/files1_sub1/files1_sub2/test120.txt @@ -0,0 +1 @@ +This is test.txt. diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files1/files1_sub1/test10.txt b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files1/files1_sub1/test10.txt new file mode 100644 index 000000000..d32727e04 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files1/files1_sub1/test10.txt @@ -0,0 +1 @@ +This is test.txt. diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files1/test1.txt b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files1/test1.txt new file mode 100644 index 000000000..d32727e04 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files1/test1.txt @@ -0,0 +1 @@ +This is test.txt. diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files1/test2.txt b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files1/test2.txt new file mode 100644 index 000000000..d32727e04 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files1/test2.txt @@ -0,0 +1 @@ +This is test.txt. diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/files2_sub2/pleasedontincludeme.dat b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/files2_sub2/pleasedontincludeme.dat new file mode 100644 index 000000000..d32727e04 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/files2_sub2/pleasedontincludeme.dat @@ -0,0 +1 @@ +This is test.txt. diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/files2_sub2/test20.txt b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/files2_sub2/test20.txt new file mode 100644 index 000000000..d32727e04 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/files2_sub2/test20.txt @@ -0,0 +1 @@ +This is test.txt. diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/files2_sub2/test21.txt b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/files2_sub2/test21.txt new file mode 100644 index 000000000..d32727e04 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/files2_sub2/test21.txt @@ -0,0 +1 @@ +This is test.txt. diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/notatest.txt b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/notatest.txt new file mode 100644 index 000000000..d32727e04 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/notatest.txt @@ -0,0 +1 @@ +This is test.txt. diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/test3.txt b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/test3.txt new file mode 100644 index 000000000..d32727e04 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/test3.txt @@ -0,0 +1 @@ +This is test.txt. diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/test4.txt b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/test4.txt new file mode 100644 index 000000000..d32727e04 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/test4.txt @@ -0,0 +1 @@ +This is test.txt.