aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBob Arnson <bob@firegiant.com>2023-10-13 18:16:08 -0400
committerBob Arnson <github@bobs.org>2024-02-26 09:18:20 -0500
commit6067839ba180f2f4bcd5eac67f839f68f513ccd2 (patch)
treef371b51c9435fe031314a882f1b68b2348ee8450
parent4f1209d8e795ddeb4c639c96081bcfebbfa8e1e2 (diff)
downloadwix-6067839ba180f2f4bcd5eac67f839f68f513ccd2.tar.gz
wix-6067839ba180f2f4bcd5eac67f839f68f513ccd2.tar.bz2
wix-6067839ba180f2f4bcd5eac67f839f68f513ccd2.zip
Add `Files` file harvesting.
Implements https://github.com/wixtoolset/issues/issues/7857. Like [naked files](https://github.com/wixtoolset/issues/issues/7696), `Files` elements can appear where `Component` elements do in WiX v4. The optimizer enumerates files and directories, generating single-file components as it goes. MSBuild-like wildcards (including `**`) are supported. `Excludes` child elements lets you exclude files that would otherwise be captured by wildcards.
-rw-r--r--src/api/wix/WixToolset.Data/ErrorMessages.cs5
-rw-r--r--src/api/wix/WixToolset.Data/Symbols/HarvestFilesSymbol.cs84
-rw-r--r--src/api/wix/WixToolset.Data/Symbols/SymbolDefinitions.cs1
-rw-r--r--src/api/wix/WixToolset.Extensibility/Services/IParseHelper.cs2
-rw-r--r--src/wix/WixToolset.Core.WindowsInstaller/Bind/CreateWindowsInstallerDataFromIRCommand.cs1
-rw-r--r--src/wix/WixToolset.Core/CommandLine/BuildCommand.cs8
-rw-r--r--src/wix/WixToolset.Core/Compiler.cs149
-rw-r--r--src/wix/WixToolset.Core/Compiler_Module.cs3
-rw-r--r--src/wix/WixToolset.Core/Compiler_Package.cs3
-rw-r--r--src/wix/WixToolset.Core/HarvestFilesCommand.cs248
-rw-r--r--src/wix/WixToolset.Core/Optimizer.cs5
-rw-r--r--src/wix/WixToolset.Core/OptimizerWarnings.cs36
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/HarvestFilesFixture.cs320
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/BadAuthoring.wxs7
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/BadDirectory.wxs14
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/BindPaths.wxs16
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/ComponentGroup.wxs16
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/Directory.wxs20
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/DirectoryRef.wxs21
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/DuplicateFiles.wxs23
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/Feature.wxs25
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/FeatureGroup.wxs25
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/FeatureRef.wxs23
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/Fragment.wxs24
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/Module.wxs6
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/PackageFiveLiner.wxs5
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/PackageWithoutDefaultFeature.wxs15
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/StandardDirectory.wxs11
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/ZeroFiles.wxs15
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files1/files1_sub1/files1_sub2/test120.txt1
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files1/files1_sub1/test10.txt1
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files1/test1.txt1
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files1/test2.txt1
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/files2_sub2/pleasedontincludeme.dat1
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/files2_sub2/test20.txt1
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/files2_sub2/test21.txt1
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/files2_sub3/FileName.Extension14
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/notatest.txt1
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/test3.txt1
-rw-r--r--src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/test4.txt1
40 files changed, 1146 insertions, 9 deletions
diff --git a/src/api/wix/WixToolset.Data/ErrorMessages.cs b/src/api/wix/WixToolset.Data/ErrorMessages.cs
index 79b835cd..d604e94f 100644
--- a/src/api/wix/WixToolset.Data/ErrorMessages.cs
+++ b/src/api/wix/WixToolset.Data/ErrorMessages.cs
@@ -368,6 +368,11 @@ namespace WixToolset.Data
368 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); 368 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);
369 } 369 }
370 370
371 public static Message ExpectedAttributeInElementOrParent(SourceLineNumber sourceLineNumbers, string elementName, string attributeName)
372 {
373 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);
374 }
375
371 public static Message ExpectedAttributeInElementOrParent(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string parentElementName) 376 public static Message ExpectedAttributeInElementOrParent(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string parentElementName)
372 { 377 {
373 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); 378 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 00000000..a3123fc1
--- /dev/null
+++ b/src/api/wix/WixToolset.Data/Symbols/HarvestFilesSymbol.cs
@@ -0,0 +1,84 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Data
4{
5 using WixToolset.Data.Symbols;
6
7 public static partial class SymbolDefinitions
8 {
9 public static readonly IntermediateSymbolDefinition HarvestFiles = new IntermediateSymbolDefinition(
10 SymbolDefinitionType.HarvestFiles,
11 new[]
12 {
13 new IntermediateFieldDefinition(nameof(HarvestFilesSymbolFields.DirectoryRef), IntermediateFieldType.String),
14 new IntermediateFieldDefinition(nameof(HarvestFilesSymbolFields.Inclusions), IntermediateFieldType.String),
15 new IntermediateFieldDefinition(nameof(HarvestFilesSymbolFields.Exclusions), IntermediateFieldType.String),
16 new IntermediateFieldDefinition(nameof(HarvestFilesSymbolFields.ComplexReferenceParentType), IntermediateFieldType.String),
17 new IntermediateFieldDefinition(nameof(HarvestFilesSymbolFields.ParentId), IntermediateFieldType.String),
18 new IntermediateFieldDefinition(nameof(HarvestFilesSymbolFields.SourcePath), IntermediateFieldType.String),
19 },
20 typeof(HarvestFilesSymbol));
21 }
22}
23
24namespace WixToolset.Data.Symbols
25{
26 public enum HarvestFilesSymbolFields
27 {
28 DirectoryRef,
29 Inclusions,
30 Exclusions,
31 ComplexReferenceParentType,
32 ParentId,
33 SourcePath,
34 }
35
36 public class HarvestFilesSymbol : IntermediateSymbol
37 {
38 public HarvestFilesSymbol() : base(SymbolDefinitions.HarvestFiles, null, null)
39 {
40 }
41
42 public HarvestFilesSymbol(SourceLineNumber sourceLineNumber, Identifier id = null) : base(SymbolDefinitions.HarvestFiles, sourceLineNumber, id)
43 {
44 }
45
46 public IntermediateField this[HarvestFilesSymbolFields index] => this.Fields[(int)index];
47
48 public string DirectoryRef
49 {
50 get => (string)this.Fields[(int)HarvestFilesSymbolFields.DirectoryRef];
51 set => this.Set((int)HarvestFilesSymbolFields.DirectoryRef, value);
52 }
53
54 public string Inclusions
55 {
56 get => (string)this.Fields[(int)HarvestFilesSymbolFields.Inclusions];
57 set => this.Set((int)HarvestFilesSymbolFields.Inclusions, value);
58 }
59
60 public string Exclusions
61 {
62 get => (string)this.Fields[(int)HarvestFilesSymbolFields.Exclusions];
63 set => this.Set((int)HarvestFilesSymbolFields.Exclusions, value);
64 }
65
66 public string ComplexReferenceParentType
67 {
68 get => (string)this.Fields[(int)HarvestFilesSymbolFields.ComplexReferenceParentType];
69 set => this.Set((int)HarvestFilesSymbolFields.ComplexReferenceParentType, value);
70 }
71
72 public string ParentId
73 {
74 get => (string)this.Fields[(int)HarvestFilesSymbolFields.ParentId];
75 set => this.Set((int)HarvestFilesSymbolFields.ParentId, value);
76 }
77
78 public string SourcePath
79 {
80 get => (string)this.Fields[(int)HarvestFilesSymbolFields.SourcePath];
81 set => this.Set((int)HarvestFilesSymbolFields.SourcePath, value);
82 }
83 }
84}
diff --git a/src/api/wix/WixToolset.Data/Symbols/SymbolDefinitions.cs b/src/api/wix/WixToolset.Data/Symbols/SymbolDefinitions.cs
index 3b545a71..67c00431 100644
--- a/src/api/wix/WixToolset.Data/Symbols/SymbolDefinitions.cs
+++ b/src/api/wix/WixToolset.Data/Symbols/SymbolDefinitions.cs
@@ -40,6 +40,7 @@ namespace WixToolset.Data
40 FeatureComponents, 40 FeatureComponents,
41 File, 41 File,
42 FileSFPCatalog, 42 FileSFPCatalog,
43 HarvestFiles,
43 Icon, 44 Icon,
44 ImageFamilies, 45 ImageFamilies,
45 IniFile, 46 IniFile,
diff --git a/src/api/wix/WixToolset.Extensibility/Services/IParseHelper.cs b/src/api/wix/WixToolset.Extensibility/Services/IParseHelper.cs
index 3c20c14b..e7856c7c 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
11 using WixToolset.Extensibility.Data; 11 using WixToolset.Extensibility.Data;
12 12
13 /// <summary> 13 /// <summary>
14 /// Interface provided to help compiler extensions parse. 14 /// Interface provided to help compiler and optimizer extensions parse.
15 /// </summary> 15 /// </summary>
16 public interface IParseHelper 16 public interface IParseHelper
17 { 17 {
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreateWindowsInstallerDataFromIRCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreateWindowsInstallerDataFromIRCommand.cs
index 5c2c40d1..cd366883 100644
--- a/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreateWindowsInstallerDataFromIRCommand.cs
+++ b/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreateWindowsInstallerDataFromIRCommand.cs
@@ -234,6 +234,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
234 break; 234 break;
235 235
236 // Symbols used internally and are not added to the output. 236 // Symbols used internally and are not added to the output.
237 case SymbolDefinitionType.HarvestFiles:
237 case SymbolDefinitionType.WixBuildInfo: 238 case SymbolDefinitionType.WixBuildInfo:
238 case SymbolDefinitionType.WixBindUpdatedFiles: 239 case SymbolDefinitionType.WixBindUpdatedFiles:
239 case SymbolDefinitionType.WixComponentGroup: 240 case SymbolDefinitionType.WixComponentGroup:
diff --git a/src/wix/WixToolset.Core/CommandLine/BuildCommand.cs b/src/wix/WixToolset.Core/CommandLine/BuildCommand.cs
index b0cd174a..cc0de13a 100644
--- a/src/wix/WixToolset.Core/CommandLine/BuildCommand.cs
+++ b/src/wix/WixToolset.Core/CommandLine/BuildCommand.cs
@@ -125,12 +125,8 @@ namespace WixToolset.Core.CommandLine
125 { 125 {
126 using (new IntermediateFieldContext("wix.link")) 126 using (new IntermediateFieldContext("wix.link"))
127 { 127 {
128 var wixipl = inputsOutputs.Wixipls.SingleOrDefault(); 128 var wixipl = inputsOutputs.Wixipls.SingleOrDefault()
129 129 ?? this.LinkPhase(wixobjs, inputsOutputs, creator, cancellationToken);
130 if (wixipl == null)
131 {
132 wixipl = this.LinkPhase(wixobjs, inputsOutputs, creator, cancellationToken);
133 }
134 130
135 if (!this.Messaging.EncounteredError) 131 if (!this.Messaging.EncounteredError)
136 { 132 {
diff --git a/src/wix/WixToolset.Core/Compiler.cs b/src/wix/WixToolset.Core/Compiler.cs
index bafe2c19..a98d4574 100644
--- a/src/wix/WixToolset.Core/Compiler.cs
+++ b/src/wix/WixToolset.Core/Compiler.cs
@@ -2655,6 +2655,9 @@ namespace WixToolset.Core
2655 case "File": 2655 case "File":
2656 this.ParseNakedFileElement(child, ComplexReferenceParentType.ComponentGroup, id.Id, directoryId, source); 2656 this.ParseNakedFileElement(child, ComplexReferenceParentType.ComponentGroup, id.Id, directoryId, source);
2657 break; 2657 break;
2658 case "Files":
2659 this.ParseFilesElement(child, ComplexReferenceParentType.ComponentGroup, id.Id, directoryId, source);
2660 break;
2658 default: 2661 default:
2659 this.Core.UnexpectedElement(node, child); 2662 this.Core.UnexpectedElement(node, child);
2660 break; 2663 break;
@@ -3106,7 +3109,7 @@ namespace WixToolset.Core
3106 this.Core.AddSymbol(new MoveFileSymbol(sourceLineNumbers, id) 3109 this.Core.AddSymbol(new MoveFileSymbol(sourceLineNumbers, id)
3107 { 3110 {
3108 ComponentRef = componentId, 3111 ComponentRef = componentId,
3109 SourceName = sourceName, 3112 SourceName = sourceName,
3110 DestinationName = destinationName, 3113 DestinationName = destinationName,
3111 DestinationShortName = destinationShortName, 3114 DestinationShortName = destinationShortName,
3112 SourceFolder = sourceDirectory ?? sourceProperty, 3115 SourceFolder = sourceDirectory ?? sourceProperty,
@@ -3881,6 +3884,9 @@ namespace WixToolset.Core
3881 case "File": 3884 case "File":
3882 this.ParseNakedFileElement(child, ComplexReferenceParentType.Unknown, null, id.Id, fileSource); 3885 this.ParseNakedFileElement(child, ComplexReferenceParentType.Unknown, null, id.Id, fileSource);
3883 break; 3886 break;
3887 case "Files":
3888 this.ParseFilesElement(child, ComplexReferenceParentType.Unknown, null, id.Id, fileSource);
3889 break;
3884 case "Merge": 3890 case "Merge":
3885 this.ParseMergeElement(child, id.Id, diskId); 3891 this.ParseMergeElement(child, id.Id, diskId);
3886 break; 3892 break;
@@ -3996,6 +4002,9 @@ namespace WixToolset.Core
3996 case "File": 4002 case "File":
3997 this.ParseNakedFileElement(child, ComplexReferenceParentType.Unknown, null, id, fileSource); 4003 this.ParseNakedFileElement(child, ComplexReferenceParentType.Unknown, null, id, fileSource);
3998 break; 4004 break;
4005 case "Files":
4006 this.ParseFilesElement(child, ComplexReferenceParentType.Unknown, null, id, fileSource);
4007 break;
3999 case "Merge": 4008 case "Merge":
4000 this.ParseMergeElement(child, id, diskId); 4009 this.ParseMergeElement(child, id, diskId);
4001 break; 4010 break;
@@ -4436,6 +4445,9 @@ namespace WixToolset.Core
4436 case "File": 4445 case "File":
4437 this.ParseNakedFileElement(child, ComplexReferenceParentType.Feature, id.Id, null, null); 4446 this.ParseNakedFileElement(child, ComplexReferenceParentType.Feature, id.Id, null, null);
4438 break; 4447 break;
4448 case "Files":
4449 this.ParseFilesElement(child, ComplexReferenceParentType.Feature, id.Id, null, null);
4450 break;
4439 case "Level": 4451 case "Level":
4440 this.ParseLevelElement(child, id.Id); 4452 this.ParseLevelElement(child, id.Id);
4441 break; 4453 break;
@@ -4579,6 +4591,9 @@ namespace WixToolset.Core
4579 case "File": 4591 case "File":
4580 this.ParseNakedFileElement(child, ComplexReferenceParentType.Feature, id, null, null); 4592 this.ParseNakedFileElement(child, ComplexReferenceParentType.Feature, id, null, null);
4581 break; 4593 break;
4594 case "Files":
4595 this.ParseFilesElement(child, ComplexReferenceParentType.Feature, id, null, null);
4596 break;
4582 case "MergeRef": 4597 case "MergeRef":
4583 this.ParseMergeRefElement(child, ComplexReferenceParentType.Feature, id); 4598 this.ParseMergeRefElement(child, ComplexReferenceParentType.Feature, id);
4584 break; 4599 break;
@@ -4667,6 +4682,9 @@ namespace WixToolset.Core
4667 case "File": 4682 case "File":
4668 this.ParseNakedFileElement(child, ComplexReferenceParentType.FeatureGroup, id.Id, null, null); 4683 this.ParseNakedFileElement(child, ComplexReferenceParentType.FeatureGroup, id.Id, null, null);
4669 break; 4684 break;
4685 case "Files":
4686 this.ParseFilesElement(child, ComplexReferenceParentType.Feature, id.Id, null, null);
4687 break;
4670 case "MergeRef": 4688 case "MergeRef":
4671 this.ParseMergeRefElement(child, ComplexReferenceParentType.FeatureGroup, id.Id); 4689 this.ParseMergeRefElement(child, ComplexReferenceParentType.FeatureGroup, id.Id);
4672 break; 4690 break;
@@ -5678,6 +5696,129 @@ namespace WixToolset.Core
5678 } 5696 }
5679 5697
5680 /// <summary> 5698 /// <summary>
5699 /// Parses a `Files` element.
5700 /// </summary>
5701 /// <param name="node">Files element to parse.</param>
5702 /// <param name="parentType">Type of complex reference parent. Will be Unknown if there is no parent.</param>
5703 /// <param name="parentId">Optional identifier for primary parent.</param>
5704 /// <param name="directoryId">Ancestor's directory id.</param>
5705 /// <param name="sourcePath">Default source path of parent directory.</param>
5706 private void ParseFilesElement(XElement node, ComplexReferenceParentType parentType, string parentId, string directoryId, string sourcePath)
5707 {
5708 var sourceLineNumbers = Preprocessor.GetSourceLineNumbers(node);
5709 var win64 = this.Context.IsCurrentPlatform64Bit;
5710 string subdirectory = null;
5711 var inclusions = new List<string>();
5712 var exclusions = new List<string>();
5713
5714 foreach (var attrib in node.Attributes())
5715 {
5716 if (String.IsNullOrEmpty(attrib.Name.NamespaceName) || CompilerCore.WixNamespace == attrib.Name.Namespace)
5717 {
5718 switch (attrib.Name.LocalName)
5719 {
5720 case "Directory":
5721 directoryId = this.Core.GetAttributeIdentifierValue(sourceLineNumbers, attrib);
5722 this.Core.CreateSimpleReference(sourceLineNumbers, SymbolDefinitions.Directory, directoryId);
5723 break;
5724 case "Subdirectory":
5725 subdirectory = this.Core.GetAttributeLongFilename(sourceLineNumbers, attrib, allowRelative: true);
5726 break;
5727 case "Include":
5728 inclusions.AddRange(this.Core.GetAttributeValue(sourceLineNumbers, attrib).Split(';'));
5729 break;
5730 default:
5731 this.Core.UnexpectedAttribute(node, attrib);
5732 break;
5733 }
5734 }
5735 else
5736 {
5737 var context = new Dictionary<string, string>() { { "Win64", win64.ToString() } };
5738 this.Core.ParseExtensionAttribute(node, attrib, context);
5739 }
5740 }
5741
5742 foreach (var child in node.Elements())
5743 {
5744 if (CompilerCore.WixNamespace == child.Name.Namespace)
5745 {
5746 switch (child.Name.LocalName)
5747 {
5748 case "Exclude":
5749 this.ParseFilesExcludeElement(child, exclusions);
5750 break;
5751 default:
5752 this.Core.UnexpectedElement(node, child);
5753 break;
5754 }
5755 }
5756 else
5757 {
5758 var context = new Dictionary<string, string>() { { "Win64", win64.ToString() } };
5759 this.Core.ParseExtensionElement(node, child, context);
5760 }
5761 }
5762
5763 if (String.IsNullOrEmpty(directoryId))
5764 {
5765 directoryId = "INSTALLFOLDER";
5766 this.Core.CreateSimpleReference(sourceLineNumbers, SymbolDefinitions.Directory, directoryId);
5767 }
5768 else if (!String.IsNullOrEmpty(subdirectory))
5769 {
5770 directoryId = this.HandleSubdirectory(sourceLineNumbers, node, directoryId, subdirectory, "Directory", "Subdirectory");
5771 }
5772
5773 if (!inclusions.Any())
5774 {
5775 this.Core.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, node.Name.LocalName, "Include"));
5776 }
5777
5778 var inclusionsAsString = String.Join(";", inclusions);
5779 var exclusionsAsString = String.Join(";", exclusions);
5780
5781 var id = this.Core.CreateIdentifier("hvf", directoryId, inclusionsAsString, exclusionsAsString);
5782
5783 this.Core.AddSymbol(new HarvestFilesSymbol(sourceLineNumbers, id)
5784 {
5785 DirectoryRef = directoryId,
5786 Inclusions = inclusionsAsString,
5787 Exclusions = exclusionsAsString,
5788 ComplexReferenceParentType = parentType.ToString(),
5789 ParentId = parentId,
5790 SourcePath = sourcePath,
5791 });
5792 }
5793
5794 private void ParseFilesExcludeElement(XElement node, IList<string> paths)
5795 {
5796 var sourceLineNumbers = Preprocessor.GetSourceLineNumbers(node);
5797
5798 foreach (var attrib in node.Attributes())
5799 {
5800 if (String.IsNullOrEmpty(attrib.Name.NamespaceName) || CompilerCore.WixNamespace == attrib.Name.Namespace)
5801 {
5802 switch (attrib.Name.LocalName)
5803 {
5804 case "Files":
5805 paths.Add(this.Core.GetAttributeValue(sourceLineNumbers, attrib));
5806 break;
5807 default:
5808 this.Core.UnexpectedAttribute(node, attrib);
5809 break;
5810 }
5811 }
5812 else
5813 {
5814 this.Core.ParseExtensionAttribute(node, attrib);
5815 }
5816 }
5817
5818 this.Core.ParseForExtensionElements(node);
5819 }
5820
5821 /// <summary>
5681 /// Parses a file search element. 5822 /// Parses a file search element.
5682 /// </summary> 5823 /// </summary>
5683 /// <param name="node">Element to parse.</param> 5824 /// <param name="node">Element to parse.</param>
@@ -5997,6 +6138,9 @@ namespace WixToolset.Core
5997 case "File": 6138 case "File":
5998 this.ParseNakedFileElement(child, ComplexReferenceParentType.Unknown, null, null, null); 6139 this.ParseNakedFileElement(child, ComplexReferenceParentType.Unknown, null, null, null);
5999 break; 6140 break;
6141 case "Files":
6142 this.ParseFilesElement(child, ComplexReferenceParentType.Unknown, null, null, null);
6143 break;
6000 case "Icon": 6144 case "Icon":
6001 this.ParseIconElement(child); 6145 this.ParseIconElement(child);
6002 break; 6146 break;
@@ -7347,6 +7491,9 @@ namespace WixToolset.Core
7347 case "File": 7491 case "File":
7348 this.ParseNakedFileElement(child, ComplexReferenceParentType.Unknown, null, id, null); 7492 this.ParseNakedFileElement(child, ComplexReferenceParentType.Unknown, null, id, null);
7349 break; 7493 break;
7494 case "Files":
7495 this.ParseFilesElement(child, ComplexReferenceParentType.Unknown, null, id, null);
7496 break;
7350 case "Merge": 7497 case "Merge":
7351 this.ParseMergeElement(child, id, diskId: CompilerConstants.IntegerNotSet); 7498 this.ParseMergeElement(child, id, diskId: CompilerConstants.IntegerNotSet);
7352 break; 7499 break;
diff --git a/src/wix/WixToolset.Core/Compiler_Module.cs b/src/wix/WixToolset.Core/Compiler_Module.cs
index 19f57773..08f47657 100644
--- a/src/wix/WixToolset.Core/Compiler_Module.cs
+++ b/src/wix/WixToolset.Core/Compiler_Module.cs
@@ -178,6 +178,9 @@ namespace WixToolset.Core
178 case "File": 178 case "File":
179 this.ParseNakedFileElement(child, ComplexReferenceParentType.Module, this.activeName, null, null); 179 this.ParseNakedFileElement(child, ComplexReferenceParentType.Module, this.activeName, null, null);
180 break; 180 break;
181 case "Files":
182 this.ParseFilesElement(child, ComplexReferenceParentType.Module, this.activeName, null, null);
183 break;
181 case "Icon": 184 case "Icon":
182 this.ParseIconElement(child); 185 this.ParseIconElement(child);
183 break; 186 break;
diff --git a/src/wix/WixToolset.Core/Compiler_Package.cs b/src/wix/WixToolset.Core/Compiler_Package.cs
index 220a2a76..8856930a 100644
--- a/src/wix/WixToolset.Core/Compiler_Package.cs
+++ b/src/wix/WixToolset.Core/Compiler_Package.cs
@@ -306,6 +306,9 @@ namespace WixToolset.Core
306 case "File": 306 case "File":
307 this.ParseNakedFileElement(child, ComplexReferenceParentType.Product, productCode, null, null); 307 this.ParseNakedFileElement(child, ComplexReferenceParentType.Product, productCode, null, null);
308 break; 308 break;
309 case "Files":
310 this.ParseFilesElement(child, ComplexReferenceParentType.Unknown, null, null, null);
311 break;
309 case "Icon": 312 case "Icon":
310 this.ParseIconElement(child); 313 this.ParseIconElement(child);
311 break; 314 break;
diff --git a/src/wix/WixToolset.Core/HarvestFilesCommand.cs b/src/wix/WixToolset.Core/HarvestFilesCommand.cs
new file mode 100644
index 00000000..c92de516
--- /dev/null
+++ b/src/wix/WixToolset.Core/HarvestFilesCommand.cs
@@ -0,0 +1,248 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Core
4{
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8 using System.Linq;
9 using WixToolset.Data;
10 using WixToolset.Data.Symbols;
11 using WixToolset.Extensibility.Data;
12 using WixToolset.Extensibility.Services;
13
14 internal class HarvestFilesCommand
15 {
16 private const string BindPathOpenString = "!(bindpath.";
17
18 public HarvestFilesCommand(IOptimizeContext context)
19 {
20 this.Context = context;
21 this.Messaging = this.Context.ServiceProvider.GetService<IMessaging>();
22 this.ParseHelper = this.Context.ServiceProvider.GetService<IParseHelper>();
23 }
24
25 public IOptimizeContext Context { get; }
26
27 public IMessaging Messaging { get; }
28
29 public IParseHelper ParseHelper { get; }
30
31 internal void Execute()
32 {
33 var harvestedFiles = new HashSet<string>();
34
35 foreach (var section in this.Context.Intermediates.SelectMany(i => i.Sections))
36 {
37 foreach (var harvestFiles in section.Symbols.OfType<HarvestFilesSymbol>().ToList())
38 {
39 this.HarvestFiles(harvestFiles, section, harvestedFiles);
40 }
41 }
42 }
43
44 private void HarvestFiles(HarvestFilesSymbol harvestFile, IntermediateSection section, ISet<string> harvestedFiles)
45 {
46 var unusedSectionCachedInlinedDirectoryIds = new Dictionary<string, string>();
47
48 var directoryId = harvestFile.DirectoryRef;
49
50 var inclusions = harvestFile.Inclusions.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
51 var exclusions = harvestFile.Exclusions.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
52
53 var comparer = new WildcardFileComparer();
54
55 var resolvedFiles = Enumerable.Empty<WildcardFile>();
56
57 try
58 {
59 var included = this.GetWildcardFiles(harvestFile, inclusions);
60 var excluded = this.GetWildcardFiles(harvestFile, exclusions);
61
62 resolvedFiles = included.Except(excluded, comparer).ToList();
63
64 if (!resolvedFiles.Any())
65 {
66 this.Messaging.Write(OptimizerWarnings.ZeroFilesHarvested(harvestFile.SourceLineNumbers));
67 }
68 }
69 catch (DirectoryNotFoundException e)
70 {
71 this.Messaging.Write(OptimizerWarnings.ExpectedDirectory(harvestFile.SourceLineNumbers, e.Message));
72
73 return;
74 }
75
76 foreach (var fileByRecursiveDir in resolvedFiles.GroupBy(resolvedFile => resolvedFile.RecursiveDir, resolvedFile => resolvedFile.Path))
77 {
78 var recursiveDir = fileByRecursiveDir.Key;
79
80 if (!String.IsNullOrEmpty(recursiveDir))
81 {
82 directoryId = this.ParseHelper.CreateDirectoryReferenceFromInlineSyntax(section, harvestFile.SourceLineNumbers, attribute: null, directoryId, recursiveDir, unusedSectionCachedInlinedDirectoryIds);
83 }
84
85 foreach (var file in fileByRecursiveDir)
86 {
87 if (harvestedFiles.Add(file))
88 {
89 var name = Path.GetFileName(file);
90
91 var id = this.ParseHelper.CreateIdentifier("fls", directoryId, name);
92
93 section.AddSymbol(new FileSymbol(harvestFile.SourceLineNumbers, id)
94 {
95 ComponentRef = id.Id,
96 Name = name,
97 Attributes = FileSymbolAttributes.None | FileSymbolAttributes.Vital,
98 DirectoryRef = directoryId,
99 Source = new IntermediateFieldPathValue { Path = file },
100 });
101
102 section.AddSymbol(new ComponentSymbol(harvestFile.SourceLineNumbers, id)
103 {
104 ComponentId = "*",
105 DirectoryRef = directoryId,
106 Location = ComponentLocation.LocalOnly,
107 KeyPath = id.Id,
108 KeyPathType = ComponentKeyPathType.File,
109 Win64 = this.Context.Platform == Platform.ARM64 || this.Context.Platform == Platform.X64,
110 });
111
112 if (Enum.TryParse<ComplexReferenceParentType>(harvestFile.ComplexReferenceParentType, out var parentType)
113 && ComplexReferenceParentType.Unknown != parentType && null != harvestFile.ParentId)
114 {
115 // If the parent was provided, add a complex reference to that, and, if
116 // the Files is under a feature, then mark the complex reference primary.
117 this.ParseHelper.CreateComplexReference(section, harvestFile.SourceLineNumbers, parentType, harvestFile.ParentId, null, ComplexReferenceChildType.Component, id.Id, ComplexReferenceParentType.Feature == parentType);
118 }
119 }
120 else
121 {
122 this.Messaging.Write(OptimizerWarnings.SkippingDuplicateFile(harvestFile.SourceLineNumbers, file));
123 }
124 }
125 }
126 }
127
128 private IEnumerable<WildcardFile> GetWildcardFiles(HarvestFilesSymbol harvestFile, IEnumerable<string> patterns)
129 {
130 var sourceLineNumbers = harvestFile.SourceLineNumbers;
131 var sourcePath = harvestFile.SourcePath;
132
133 var files = new List<WildcardFile>();
134
135 foreach (var pattern in patterns)
136 {
137 // Resolve bind paths, if any, which might result in multiple directories.
138 foreach (var path in this.ResolveBindPaths(sourceLineNumbers, pattern))
139 {
140 var sourceDirectory = String.IsNullOrEmpty(sourcePath) ? Path.GetDirectoryName(sourceLineNumbers.FileName) : sourcePath;
141 var recursive = path.IndexOf("**") >= 0;
142 var filePortion = Path.GetFileName(path);
143 var directoryPortion = Path.GetDirectoryName(path);
144
145 if (directoryPortion?.EndsWith(@"\**") == true)
146 {
147 directoryPortion = directoryPortion.Substring(0, directoryPortion.Length - 3);
148 }
149
150 var recursiveDirOffset = directoryPortion.Length + 1;
151
152 if (directoryPortion is null || directoryPortion.Length == 0 || directoryPortion == "**")
153 {
154 directoryPortion = sourceDirectory;
155 recursiveDirOffset = sourceDirectory.Length + 1;
156
157 }
158 else if (!Path.IsPathRooted(directoryPortion))
159 {
160 directoryPortion = Path.Combine(sourceDirectory, directoryPortion);
161 recursiveDirOffset = sourceDirectory.Length + 1;
162 }
163
164 var foundFiles = Directory.EnumerateFiles(directoryPortion, filePortion, recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
165
166 foreach (var foundFile in foundFiles)
167 {
168 var recursiveDir = Path.GetDirectoryName(foundFile.Substring(recursiveDirOffset));
169 files.Add(new WildcardFile()
170 {
171 RecursiveDir = recursiveDir,
172 Path = foundFile,
173 });
174 }
175 }
176 }
177
178 return files;
179 }
180
181 private IEnumerable<string> ResolveBindPaths(SourceLineNumber sourceLineNumbers, string source)
182 {
183 var resultingDirectories = new List<string>();
184
185 var bindName = String.Empty;
186 var path = source;
187
188 if (source.StartsWith(BindPathOpenString, StringComparison.Ordinal))
189 {
190 var closeParen = source.IndexOf(')', BindPathOpenString.Length);
191
192 if (-1 != closeParen)
193 {
194 bindName = source.Substring(BindPathOpenString.Length, closeParen - BindPathOpenString.Length);
195 path = source.Substring(BindPathOpenString.Length + bindName.Length + 1); // +1 for the closing brace.
196 path = path.TrimStart('\\'); // remove starting '\\' char so the path doesn't look rooted.
197 }
198 }
199
200 if (String.IsNullOrEmpty(bindName))
201 {
202 resultingDirectories.Add(path);
203 }
204 else
205 {
206 var foundBindPath = false;
207
208 foreach (var bindPath in this.Context.BindPaths)
209 {
210 if (bindName.Equals(bindPath.Name, StringComparison.OrdinalIgnoreCase))
211 {
212 var resolved = Path.Combine(bindPath.Path, path);
213 resultingDirectories.Add(resolved);
214
215 foundBindPath = true;
216 }
217 }
218
219 if (!foundBindPath)
220 {
221 this.Messaging.Write(OptimizerWarnings.ExpectedDirectory(sourceLineNumbers, source));
222 }
223 }
224
225 return resultingDirectories;
226 }
227
228 private class WildcardFile
229 {
230 public string RecursiveDir { get; set; }
231
232 public string Path { get; set; }
233 }
234
235 private class WildcardFileComparer : IEqualityComparer<WildcardFile>
236 {
237 public bool Equals(WildcardFile x, WildcardFile y)
238 {
239 return x?.Path == y?.Path;
240 }
241
242 public int GetHashCode(WildcardFile obj)
243 {
244 return obj?.Path?.GetHashCode() ?? 0;
245 }
246 }
247 }
248}
diff --git a/src/wix/WixToolset.Core/Optimizer.cs b/src/wix/WixToolset.Core/Optimizer.cs
index 5864121e..33f757a3 100644
--- a/src/wix/WixToolset.Core/Optimizer.cs
+++ b/src/wix/WixToolset.Core/Optimizer.cs
@@ -25,7 +25,10 @@ namespace WixToolset.Core
25 extension.PreOptimize(context); 25 extension.PreOptimize(context);
26 } 26 }
27 27
28 // TODO: Fill with useful optimization features. 28 {
29 var command = new HarvestFilesCommand(context);
30 command.Execute();
31 }
29 32
30 foreach (var extension in context.Extensions) 33 foreach (var extension in context.Extensions)
31 { 34 {
diff --git a/src/wix/WixToolset.Core/OptimizerWarnings.cs b/src/wix/WixToolset.Core/OptimizerWarnings.cs
new file mode 100644
index 00000000..784dc587
--- /dev/null
+++ b/src/wix/WixToolset.Core/OptimizerWarnings.cs
@@ -0,0 +1,36 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Core
4{
5 using WixToolset.Data;
6
7 internal static class OptimizerWarnings
8 {
9 public static Message ZeroFilesHarvested(SourceLineNumber sourceLineNumbers)
10 {
11 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.");
12 }
13
14 public static Message ExpectedDirectory(SourceLineNumber sourceLineNumbers, string harvestDirectory)
15 {
16 return Message(sourceLineNumbers, Ids.ExpectedDirectory, "Missing directory for harvesting files: {0}", harvestDirectory);
17 }
18
19 public static Message SkippingDuplicateFile(SourceLineNumber sourceLineNumbers, string duplicateFile)
20 {
21 return Message(sourceLineNumbers, Ids.SkippingDuplicateFile, "Skipping file that has already been harvested: {0}", duplicateFile);
22 }
23
24 private static Message Message(SourceLineNumber sourceLineNumber, Ids id, string format, params object[] args)
25 {
26 return new Message(sourceLineNumber, MessageLevel.Warning, (int)id, format, args);
27 }
28
29 public enum Ids
30 {
31 ZeroFilesHarvested = 8600,
32 ExpectedDirectory = 8601,
33 SkippingDuplicateFile = 8602,
34 }
35 }
36}
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/HarvestFilesFixture.cs b/src/wix/test/WixToolsetTest.CoreIntegration/HarvestFilesFixture.cs
new file mode 100644
index 00000000..c9e7fb14
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/HarvestFilesFixture.cs
@@ -0,0 +1,320 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolsetTest.CoreIntegration
4{
5 using System.Data;
6 using System.IO;
7 using System.Linq;
8 using WixInternal.Core.TestPackage;
9 using WixInternal.TestSupport;
10 using Xunit;
11
12 public class HarvestFilesFixture
13 {
14 [Fact]
15 public void MustIncludeSomeFiles()
16 {
17 var messages = BuildAndQueryComponentAndFileTables("BadAuthoring.wxs", isPackage: true, 10);
18 Assert.Equal(new[]
19 {
20 "10",
21 }, messages);
22 }
23
24 [Fact]
25 public void ZeroFilesHarvestedIsAWarning()
26 {
27 var messages = BuildAndQueryComponentAndFileTables("ZeroFiles.wxs", isPackage: true, 8600);
28 Assert.Equal(new[]
29 {
30 "8600",
31 }, messages);
32 }
33
34 [Fact]
35 public void MissingHarvestDirectoryIsAWarning()
36 {
37 var messages = BuildAndQueryComponentAndFileTables("BadDirectory.wxs", isPackage: true, 8601);
38 Assert.Equal(new[]
39 {
40 "8601",
41 "8601",
42 }, messages);
43 }
44
45 [Fact]
46 public void DuplicateFilesSomethingSomething()
47 {
48 var messages = BuildAndQueryComponentAndFileTables("DuplicateFiles.wxs", isPackage: true, 8602);
49 Assert.Equal(new[]
50 {
51 "8602",
52 "8602",
53 "8602",
54 "8602",
55 }, messages);
56 }
57
58 [Fact]
59 public void CanHarvestFilesInComponentGroup()
60 {
61 BuildQueryAssertFiles("ComponentGroup.wxs", new[]
62 {
63 "FileName.Extension",
64 "test20.txt",
65 "test21.txt",
66 "test3.txt",
67 "test4.txt",
68 });
69 }
70
71 [Fact]
72 public void CanHarvestFilesInDirectory()
73 {
74 BuildQueryAssertFiles("Directory.wxs", new[]
75 {
76 "test10.txt",
77 "test120.txt",
78 "test2.txt",
79 "test3.txt",
80 "test4.txt",
81 });
82 }
83
84 [Fact]
85 public void CanHarvestFilesInDirectoryRef()
86 {
87 BuildQueryAssertFiles("DirectoryRef.wxs", new[]
88 {
89 "notatest.txt",
90 "pleasedontincludeme.dat",
91 "test1.txt",
92 "test120.txt",
93 "test2.txt",
94 "test20.txt",
95 "test21.txt",
96 "test3.txt",
97 "test4.txt",
98 });
99 }
100
101 [Fact]
102 public void CanHarvestFilesInFeature()
103 {
104 var rows = BuildAndQueryComponentAndFileTables("Feature.wxs");
105
106 AssertFileComponentIds(3, rows);
107 }
108
109 [Fact]
110 public void CanHarvestFilesInFeatureGroup()
111 {
112 BuildQueryAssertFiles("FeatureGroup.wxs", new[]
113 {
114 "FileName.Extension",
115 "notatest.txt",
116 "pleasedontincludeme.dat",
117 "test1.txt",
118 "test2.txt",
119 "test20.txt",
120 "test21.txt",
121 "test3.txt",
122 "test4.txt",
123 });
124 }
125
126 [Fact]
127 public void CanHarvestFilesInFeatureRef()
128 {
129 BuildQueryAssertFiles("FeatureRef.wxs", new[]
130 {
131 "FileName.Extension",
132 "notatest.txt",
133 "pleasedontincludeme.dat",
134 "test1.txt",
135 "test2.txt",
136 "test20.txt",
137 "test21.txt",
138 "test3.txt",
139 "test4.txt",
140 });
141 }
142
143 [Fact]
144 public void CanHarvestFilesInFragments()
145 {
146 BuildQueryAssertFiles("Fragment.wxs", new[]
147 {
148 "notatest.txt",
149 "test1.txt",
150 "test2.txt",
151 "test3.txt",
152 "test4.txt",
153 });
154 }
155
156 [Fact]
157 public void CanHarvestFilesInModules()
158 {
159 BuildQueryAssertFiles("Module.wxs", new[]
160 {
161 "notatest.txt",
162 "test1.txt",
163 "test2.txt",
164 "test3.txt",
165 "test4.txt",
166 }, isPackage: false);
167 }
168
169 [Fact]
170 public void CanHarvestFilesWithBindPaths()
171 {
172 BuildQueryAssertFiles("BindPaths.wxs", new[]
173 {
174 "FileName.Extension",
175 "test1.txt",
176 "test10.txt",
177 "test120.txt",
178 "test2.txt",
179 "test20.txt",
180 "test21.txt",
181 "test3.txt",
182 "test4.txt",
183 });
184 }
185
186 [Fact]
187 public void HarvestedFilesUnderPackageWithAuthoredFeatureAreOrphaned()
188 {
189 var messages = BuildAndQueryComponentAndFileTables("PackageWithoutDefaultFeature.wxs", isPackage: true, 267);
190 Assert.Equal(new[]
191 {
192 "267",
193 "267",
194 "267",
195 "267",
196 }, messages);
197 }
198
199 [Fact]
200 public void CanHarvestFilesInStandardDirectory()
201 {
202 BuildQueryAssertFiles("StandardDirectory.wxs", new[]
203 {
204 "FileName.Extension",
205 "notatest.txt",
206 "pleasedontincludeme.dat",
207 "test1.txt",
208 "test10.txt",
209 "test120.txt",
210 "test2.txt",
211 "test20.txt",
212 "test21.txt",
213 "test3.txt",
214 "test4.txt",
215 });
216 }
217
218 [Fact]
219 public void CanHarvestFilesInFiveLines()
220 {
221 BuildQueryAssertFiles("PackageFiveLiner.wxs", new[]
222 {
223 "FileName.Extension",
224 "notatest.txt",
225 "pleasedontincludeme.dat",
226 "test20.txt",
227 "test21.txt",
228 "test3.txt",
229 "test4.txt",
230 });
231 }
232
233 private static void BuildQueryAssertFiles(string file, string[] expectedFileNames, bool isPackage = true, int? exitCode = null)
234 {
235 var rows = BuildAndQueryComponentAndFileTables(file, isPackage, exitCode);
236
237 var fileNames = AssertFileComponentIds(expectedFileNames.Length, rows);
238
239 Assert.Equal(expectedFileNames, fileNames);
240 }
241
242 private static string[] BuildAndQueryComponentAndFileTables(string file, bool isPackage = true, int? exitCode = null)
243 {
244 var folder = TestData.Get("TestData", "HarvestFiles");
245
246 using (var fs = new DisposableFileSystem())
247 {
248 var baseFolder = fs.GetFolder();
249 var intermediateFolder = Path.Combine(baseFolder, "obj");
250 var binFolder = Path.Combine(baseFolder, "bin");
251 var msiPath = Path.Combine(binFolder, isPackage ? "test.msi" : "test.msm");
252
253 var arguments = new[]
254 {
255 "build",
256 Path.Combine(folder, file),
257 "-intermediateFolder", intermediateFolder,
258 "-bindpath", folder,
259 "-bindpath", @$"ToBeHarvested={folder}\files1",
260 "-bindpath", @$"ToBeHarvested={folder}\files2",
261 "-o", msiPath,
262 };
263
264 var result = WixRunner.Execute(arguments);
265
266 if (exitCode.HasValue)
267 {
268 Assert.Equal(exitCode.Value, result.ExitCode);
269
270 return result.Messages.Select(m => m.Id.ToString()).ToArray();
271 }
272 else
273 {
274 result.AssertSuccess();
275
276 return Query.QueryDatabase(msiPath, new[] { "Component", "File" })
277 .OrderBy(s => s)
278 .ToArray();
279 }
280 }
281 }
282
283 private static string[] AssertFileComponentIds(int fileCount, string[] rows)
284 {
285 var componentRows = rows.Where(row => row.StartsWith("Component:")).ToArray();
286 var fileRows = rows.Where(row => row.StartsWith("File:")).ToArray();
287
288 Assert.Equal(componentRows.Length, fileRows.Length);
289
290 // Component id == Component keypath == File id
291 foreach (var componentRow in componentRows)
292 {
293 var columns = componentRow.Split(':', '\t');
294 Assert.Equal(columns[1], columns[6]);
295 }
296
297 foreach (var fileRow in fileRows)
298 {
299 var columns = fileRow.Split(':', '\t');
300 Assert.Equal(columns[1], columns[2]);
301 }
302
303 Assert.Equal(fileCount, componentRows.Length);
304
305 var files = fileRows.Select(row => row.Split('\t')[2]);
306 var lfns = files.Select(name => name.Split('|'));
307
308 return fileRows
309 .Select(row => row.Split('\t')[2])
310 .Select(GetLFN)
311 .OrderBy(name => name).ToArray();
312
313 static string GetLFN(string possibleSfnLfnPair)
314 {
315 var parts = possibleSfnLfnPair.Split('|');
316 return parts[parts.Length - 1];
317 }
318 }
319 }
320}
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 00000000..c8f25e1d
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/BadAuthoring.wxs
@@ -0,0 +1,7 @@
1<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
2 <Package Name="MsiPackage" Version="1.0.0.0" Manufacturer="Example Corporation" UpgradeCode="047730a5-30fe-4a62-a520-da9381b8226a">
3 <ComponentGroup Id="Files">
4 <Files />
5 </ComponentGroup>
6 </Package>
7</Wix>
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 00000000..88cbb3eb
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/BadDirectory.wxs
@@ -0,0 +1,14 @@
1<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
2 <Package Name="MsiPackage" Version="1.0.0.0" Manufacturer="Example Corporation" UpgradeCode="047730a5-30fe-4a62-a520-da9381b8226a">
3 <MajorUpgrade DowngradeErrorMessage="Downgrade error message." />
4
5 <Feature Id="ProductFeature">
6 <ComponentGroupRef Id="Files" />
7 </Feature>
8
9 <ComponentGroup Id="Files" Directory="ProgramFilesFolder" Subdirectory="MsiPackage" Source="$(sys.SOURCEFILEDIR)files2">
10 <Files Include="MissingDirectory\**" />
11 <Files Include="ThisDirectoryIsAlsoMissing\**" />
12 </ComponentGroup>
13 </Package>
14</Wix>
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 00000000..b0495125
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/BindPaths.wxs
@@ -0,0 +1,16 @@
1<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
2 <Package Name="HarvestedFiles" Version="1.0.0.0" Manufacturer="Example Corporation" UpgradeCode="047730a5-30fe-4a62-a520-da9381b8226a">
3 <MajorUpgrade DowngradeErrorMessage="Downgrade error message." />
4
5 <Feature Id="ProductFeature">
6 <ComponentGroupRef Id="Files" />
7 </Feature>
8
9 <ComponentGroup Id="Files" Directory="ProgramFilesFolder" Subdirectory="HarvestedFiles">
10 <Files Include="!(bindpath.ToBeHarvested)\**">
11 <Exclude Files="!(bindpath.ToBeHarvested)\notatest.txt" />
12 <Exclude Files="**\pleasedontincludeme.dat" />
13 </Files>
14 </ComponentGroup>
15 </Package>
16</Wix>
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 00000000..963f40a5
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/ComponentGroup.wxs
@@ -0,0 +1,16 @@
1<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
2 <Package Name="MsiPackage" Version="1.0.0.0" Manufacturer="Example Corporation" UpgradeCode="047730a5-30fe-4a62-a520-da9381b8226a">
3 <MajorUpgrade DowngradeErrorMessage="Downgrade error message." />
4
5 <Feature Id="ProductFeature">
6 <ComponentGroupRef Id="Files" />
7 </Feature>
8
9 <ComponentGroup Id="Files" Directory="ProgramFilesFolder" Subdirectory="MsiPackage" Source="$(sys.SOURCEFILEDIR)files2">
10 <Files Include="**">
11 <Exclude Files="notatest.txt" />
12 <Exclude Files="files2_sub2\pleasedontincludeme.dat" />
13 </Files>
14 </ComponentGroup>
15 </Package>
16</Wix>
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 00000000..362c5da1
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/Directory.wxs
@@ -0,0 +1,20 @@
1<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
2 <Package Name="MsiPackage" Version="1.0.0.0" Manufacturer="Example Corporation" UpgradeCode="047730a5-30fe-4a62-a520-da9381b8226a">
3 <MajorUpgrade DowngradeErrorMessage="Downgrade error message." />
4
5 <StandardDirectory Id="ProgramFilesFolder">
6 <Directory Id="INSTALLFOLDER" Name="MsiPackage" FileSource="$(sys.SOURCEFILEDIR)">
7 <!-- Relies on default-feature feature to include naked files in package. -->
8 <Files Include="files1\**">
9 <Exclude Files="files1\test1.txt" />
10 </Files>
11
12 <Files Include="files2\**">
13 <Exclude Files="**\*.Extension" />
14 <Exclude Files="files2\notatest.txt" />
15 <Exclude Files="files2\files2_sub2\**" />
16 </Files>
17 </Directory>
18 </StandardDirectory>
19 </Package>
20</Wix>
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 00000000..3e475ff9
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/DirectoryRef.wxs
@@ -0,0 +1,21 @@
1<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
2 <Package Name="MsiPackage" Version="1.0.0.0" Manufacturer="Example Corporation" UpgradeCode="047730a5-30fe-4a62-a520-da9381b8226a">
3 <MajorUpgrade DowngradeErrorMessage="Downgrade error message." />
4
5 <DirectoryRef Id="INSTALLFOLDER">
6 <!-- Relies on default-feature feature to include naked files in package. -->
7 <Files Include="files1\**">
8 <Exclude Files="files1\files1_sub1\*" />
9 </Files>
10 <Files Include="files2\**">
11 <Exclude Files="$(sys.SOURCEFILEDIR)\files2\**.extension" />
12 </Files>
13 </DirectoryRef>
14 </Package>
15
16 <Fragment>
17 <StandardDirectory Id="ProgramFilesFolder">
18 <Directory Id="INSTALLFOLDER" Name="MsiPackage" />
19 </StandardDirectory>
20 </Fragment>
21</Wix>
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 00000000..8375e0ae
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/DuplicateFiles.wxs
@@ -0,0 +1,23 @@
1<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
2 <Package Name="MsiPackage" Version="1.0.0.0" Manufacturer="Example Corporation" UpgradeCode="047730a5-30fe-4a62-a520-da9381b8226a">
3 <MajorUpgrade DowngradeErrorMessage="Downgrade error message." />
4
5 <Feature Id="ProductFeature">
6 <ComponentGroupRef Id="FilesA" />
7 <ComponentGroupRef Id="FilesB" />
8 </Feature>
9
10 </Package>
11
12 <Fragment>
13 <ComponentGroup Id="FilesA" Directory="ProgramFilesFolder" Subdirectory="MsiPackage">
14 <Files Include="files1\**" />
15 </ComponentGroup>
16 </Fragment>
17
18 <Fragment>
19 <ComponentGroup Id="FilesB" Directory="ProgramFilesFolder" Subdirectory="MsiPackage">
20 <Files Include="files1\**" />
21 </ComponentGroup>
22 </Fragment>
23</Wix>
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 00000000..05152d72
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/Feature.wxs
@@ -0,0 +1,25 @@
1<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
2 <Package Name="MsiPackage" Version="1.0.0.0" Manufacturer="Example Corporation" UpgradeCode="047730a5-30fe-4a62-a520-da9381b8226a">
3 <MajorUpgrade DowngradeErrorMessage="Downgrade error message." />
4
5 <Feature Id="ProductFeature">
6 <Files
7 Directory="ProgramFiles6432Folder"
8 Subdirectory="Example Product"
9 Include="files1\*">
10 <Exclude Files="files1\test1.txt" />
11 </Files>
12
13 <!--
14 `$(sys.SOURCEFILEDIR)` is equivalent to the above (i.e., the default),
15 but this validates that preprocessor variables are happy here.
16 -->
17 <Files
18 Directory="ProgramFiles6432Folder"
19 Subdirectory="Example Product\Assets"
20 Include="$(sys.SOURCEFILEDIR)\files2\*">
21 <Exclude Files="$(sys.SOURCEFILEDIR)\files2\notatest.txt" />
22 </Files>
23 </Feature>
24 </Package>
25</Wix>
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 00000000..5c1b2165
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/FeatureGroup.wxs
@@ -0,0 +1,25 @@
1<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
2 <Package Name="MsiPackage" Version="1.0.0.0" Manufacturer="Example Corporation" UpgradeCode="047730a5-30fe-4a62-a520-da9381b8226a">
3 <MajorUpgrade DowngradeErrorMessage="Downgrade error message." />
4
5 <Feature Id="ProductFeature">
6 <FeatureGroupRef Id="ProductFeatureGroup" />
7 </Feature>
8 </Package>
9
10 <Fragment>
11 <StandardDirectory Id="ProgramFilesFolder">
12 <Directory Id="INSTALLFOLDER" Name="MsiPackage" />
13 </StandardDirectory>
14 </Fragment>
15
16 <Fragment>
17 <FeatureGroup Id="ProductFeatureGroup">
18 <Files Directory="INSTALLFOLDER" Include="files1\**">
19 <Exclude Files="files1\files1_sub1\**" />
20 </Files>
21
22 <Files Directory="INSTALLFOLDER" Subdirectory="assets" Include="files2\**" />
23 </FeatureGroup>
24 </Fragment>
25</Wix>
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 00000000..86a90e29
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/FeatureRef.wxs
@@ -0,0 +1,23 @@
1<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
2 <Package Name="MsiPackage" Version="1.0.0.0" Manufacturer="Example Corporation" UpgradeCode="047730a5-30fe-4a62-a520-da9381b8226a">
3 <MajorUpgrade DowngradeErrorMessage="Downgrade error message." />
4
5 <FeatureRef Id="ProductFeature">
6 <Files Directory="INSTALLFOLDER" Include="files1\**">
7 <Exclude Files="files1\files1_sub1\**" />
8 </Files>
9
10 <Files Directory="INSTALLFOLDER" Subdirectory="assets" Include="files2\**" />
11 </FeatureRef>
12 </Package>
13
14 <Fragment>
15 <StandardDirectory Id="ProgramFilesFolder">
16 <Directory Id="INSTALLFOLDER" Name="MsiPackage" />
17 </StandardDirectory>
18 </Fragment>
19
20 <Fragment>
21 <Feature Id="ProductFeature" />
22 </Fragment>
23</Wix>
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 00000000..6f5053d2
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/Fragment.wxs
@@ -0,0 +1,24 @@
1<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
2 <Package Name="MsiPackage" Version="1.0.0.0" Manufacturer="Example Corporation" UpgradeCode="047730a5-30fe-4a62-a520-da9381b8226a">
3 <MajorUpgrade DowngradeErrorMessage="Downgrade error message." />
4
5 <FeatureGroupRef Id="FeatureGroup1" />
6 <FeatureGroupRef Id="FeatureGroup2" />
7 </Package>
8
9 <Fragment>
10 <StandardDirectory Id="ProgramFilesFolder">
11 <Directory Id="INSTALLFOLDER" Name="MsiPackage" />
12 </StandardDirectory>
13 </Fragment>
14
15 <Fragment>
16 <FeatureGroup Id="FeatureGroup1" />
17 <Files Directory="ProgramFilesFolder" Subdirectory="MsiPackage" Include="files1\*" />
18 </Fragment>
19
20 <Fragment>
21 <FeatureGroup Id="FeatureGroup2" />
22 <Files Directory="ProgramFilesFolder" Subdirectory="MsiPackage" Include="files2\*" />
23 </Fragment>
24</Wix>
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 00000000..7034aa9d
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/Module.wxs
@@ -0,0 +1,6 @@
1<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
2 <Module Id="MergeModule" Guid="e535b765-1019-4a4f-b3ea-ae28870e6d73" Language="1033" Version="1.0.0.0">
3 <Files Directory="ProgramFilesFolder" Subdirectory="MergeModule" Include="files1\*" />
4 <Files Directory="ProgramFilesFolder" Subdirectory="MergeModule" Include="files2\*" />
5 </Module>
6</Wix>
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/PackageFiveLiner.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/PackageFiveLiner.wxs
new file mode 100644
index 00000000..7bf0845c
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/PackageFiveLiner.wxs
@@ -0,0 +1,5 @@
1<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
2 <Package Name="MsiPackage" Version="1.0.0.0" Manufacturer="Example Corporation" UpgradeCode="047730a5-30fe-4a62-a520-da9381b8226a">
3 <Files Include="files2\**" />
4 </Package>
5</Wix>
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 00000000..3157202a
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/PackageWithoutDefaultFeature.wxs
@@ -0,0 +1,15 @@
1<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
2 <Package Name="MsiPackage" Version="1.0.0.0" Manufacturer="Example Corporation" UpgradeCode="047730a5-30fe-4a62-a520-da9381b8226a">
3 <MajorUpgrade DowngradeErrorMessage="Downgrade error message." />
4
5 <Files Directory="ProgramFilesFolder" Subdirectory="MsiPackage" Include="files1\**" />
6
7 <Feature Id="ProductFeature">
8 <ComponentGroupRef Id="Files" />
9 </Feature>
10
11 <ComponentGroup Id="Files" Directory="ProgramFilesFolder" Subdirectory="MsiPackage">
12 <Files Directory="ProgramFilesFolder" Subdirectory="MsiPackage" Include="files2\*" />
13 </ComponentGroup>
14 </Package>
15</Wix>
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 00000000..1838ed66
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/StandardDirectory.wxs
@@ -0,0 +1,11 @@
1<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
2 <Package Name="MsiPackage" Version="1.0.0.0" Manufacturer="Example Corporation" UpgradeCode="047730a5-30fe-4a62-a520-da9381b8226a">
3 <MajorUpgrade DowngradeErrorMessage="Downgrade error message." />
4
5 <StandardDirectory Id="ProgramFiles6432Folder">
6 <!-- Relies on default-feature feature to include naked files in package. -->
7 <Files Subdirectory="MsiPackage" Include="files1\**" />
8 <Files Subdirectory="MsiPackage" Include="files2\**" />
9 </StandardDirectory>
10 </Package>
11</Wix>
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 00000000..e733622f
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/ZeroFiles.wxs
@@ -0,0 +1,15 @@
1<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
2 <Package Name="MsiPackage" Version="1.0.0.0" Manufacturer="Example Corporation" UpgradeCode="047730a5-30fe-4a62-a520-da9381b8226a">
3 <MajorUpgrade DowngradeErrorMessage="Downgrade error message." />
4
5 <Feature Id="ProductFeature">
6 <ComponentGroupRef Id="Files" />
7 </Feature>
8
9 <ComponentGroup Id="Files" Directory="ProgramFilesFolder" Subdirectory="MsiPackage" Source="$(sys.SOURCEFILEDIR)files2">
10 <Files Include="**">
11 <Exclude Files="**" />
12 </Files>
13 </ComponentGroup>
14 </Package>
15</Wix>
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 00000000..d32727e0
--- /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 00000000..d32727e0
--- /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 00000000..d32727e0
--- /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 00000000..d32727e0
--- /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 00000000..d32727e0
--- /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 00000000..d32727e0
--- /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 00000000..d32727e0
--- /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/files2_sub3/FileName.Extension b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/files2_sub3/FileName.Extension
new file mode 100644
index 00000000..6f2d4e96
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/files2_sub3/FileName.Extension
@@ -0,0 +1,14 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolsetTest.CoreIntegration.TestData.HarvestFiles.files2.files2_sub3
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Linq;
8 using System.Text;
9 using System.Threading.Tasks;
10
11 internal class FileName
12 {
13 }
14}
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 00000000..d32727e0
--- /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 00000000..d32727e0
--- /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 00000000..d32727e0
--- /dev/null
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/HarvestFiles/files2/test4.txt
@@ -0,0 +1 @@
This is test.txt.