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.