Add ability for extensions to create custom bundle searches. This required creating BundleExtensionData.xml.
Add ability for extensions to create custom bundle searches. This required creating BundleExtensionData.xml.
Sean Hall committed
Mar 27, 2020 at 13:54 UTC
0baf6e26ec7ab2ff0b6ad36e9d44f3d68819b5d6
23 files changed
+688
-77
src/WixToolset.Core.Burn/Bind/BaseSearchFacade.cs
new
+27
@@ -0,0 +1,27 @@
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
+
3
+namespace WixToolset.Core.Burn
4
+{
5
+ using System;
6
+ using System.Xml;
7
+ using WixToolset.Data.Tuples;
8
+
9
+ internal abstract class BaseSearchFacade : ISearchFacade
10
+ {
11
+ protected WixSearchTuple SearchTuple { get; set; }
12
+
13
+ public virtual void WriteXml(XmlTextWriter writer)
14
+ {
15
+ writer.WriteAttributeString("Id", this.SearchTuple.Id.Id);
16
+ writer.WriteAttributeString("Variable", this.SearchTuple.Variable);
17
+ if (!String.IsNullOrEmpty(this.SearchTuple.Condition))
18
+ {
19
+ writer.WriteAttributeString("Condition", this.SearchTuple.Condition);
20
+ }
21
+ if (!String.IsNullOrEmpty(this.SearchTuple.BundleExtensionRef))
22
+ {
23
+ writer.WriteAttributeString("ExtensionId", this.SearchTuple.BundleExtensionRef);
24
+ }
25
+ }
26
+ }
27
+}
src/WixToolset.Core.Burn/Bind/BindBundleCommand.cs
+15
-26
@@ -117,10 +117,10 @@ namespace WixToolset.Core.Burn
117
// If there are any fields to resolve later, create the cache to populate during bind.
118
var variableCache = this.DelayedFields.Any() ? new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase) : null;
119
120
- // TODO: Although the WixSearch tables are defined in the Util extension,
121
- // the Bundle Binder has to know all about them. We hope to revisit all
122
- // of this in the 4.0 timeframe.
123
- var orderedSearches = this.OrderSearches(section);
120
+ var orderSearchesCommand = new OrderSearchesCommand(this.Messaging, section);
121
+ orderSearchesCommand.Execute();
122
+ var orderedSearches = orderSearchesCommand.OrderedSearchFacades;
123
+ var extensionSearchTuplesById = orderSearchesCommand.ExtensionSearchTuplesByExtensionId;
124
125
// Extract files that come from binary .wixlibs and WixExtensions (this does not extract files from merge modules).
126
{
@@ -387,6 +387,17 @@ namespace WixToolset.Core.Burn
387
388
var baManifestPayload = command.BootstrapperApplicationManifestPayloadRow;
389
payloadTuples.Add(baManifestPayload.Id.Id, baManifestPayload);
390
+ ++uxPayloadIndex;
391
+ }
392
+
393
+ // Generate the bundle extension manifest...
394
+ {
395
+ var command = new CreateBundleExtensionManifestCommand(section, bundleTuple, extensionSearchTuplesById, uxPayloadIndex, this.IntermediateFolder);
396
+ command.Execute();
397
+
398
+ var bextManifestPayload = command.BundleExtensionManifestPayloadRow;
399
+ payloadTuples.Add(bextManifestPayload.Id.Id, bextManifestPayload);
400
+ ++uxPayloadIndex;
401
}
402
403
#if TODO
@@ -464,28 +475,6 @@ namespace WixToolset.Core.Burn
475
trackedFiles.Add(trackIntermediate);
476
}
477
467
- private IEnumerable<SearchFacade> OrderSearches(IntermediateSection section)
468
- {
469
- var searchesById = section.Tuples
470
- .Where(t => t.Definition.Type == TupleDefinitionType.WixComponentSearch ||
471
- t.Definition.Type == TupleDefinitionType.WixFileSearch ||
472
- t.Definition.Type == TupleDefinitionType.WixProductSearch ||
473
- t.Definition.Type == TupleDefinitionType.WixRegistrySearch)
474
- .ToDictionary(t => t.Id.Id);
475
-
476
- var orderedSearches = new List<SearchFacade>(searchesById.Keys.Count);
477
-
478
- foreach (var searchTuple in section.Tuples.OfType<WixSearchTuple>())
479
- {
480
- if (searchesById.TryGetValue(searchTuple.Id.Id, out var specificSearchTuple))
481
- {
482
- orderedSearches.Add(new SearchFacade(searchTuple, specificSearchTuple));
483
- }
484
- }
485
-
486
- return orderedSearches;
487
- }
488
-
478
/// <summary>
479
/// Populates the variable cache with specific package properties.
480
/// </summary>
src/WixToolset.Core.Burn/Bind/ExtensionSearchFacade.cs
new
+24
@@ -0,0 +1,24 @@
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
+
3
+namespace WixToolset.Core.Burn
4
+{
5
+ using System.Xml;
6
+ using WixToolset.Data.Tuples;
7
+
8
+ internal class ExtensionSearchFacade : BaseSearchFacade
9
+ {
10
+ public ExtensionSearchFacade(WixSearchTuple searchTuple)
11
+ {
12
+ this.SearchTuple = searchTuple;
13
+ }
14
+
15
+ public override void WriteXml(XmlTextWriter writer)
16
+ {
17
+ writer.WriteStartElement("ExtensionSearch");
18
+
19
+ base.WriteXml(writer);
20
+
21
+ writer.WriteEndElement();
22
+ }
23
+ }
24
+}
src/WixToolset.Core.Burn/Bind/LegacySearchFacade.cs
renamed
+31
-43
@@ -7,48 +7,36 @@ namespace WixToolset.Core.Burn
7
using WixToolset.Data;
8
using WixToolset.Data.Tuples;
9
10
- internal class SearchFacade
10
+ internal class LegacySearchFacade : BaseSearchFacade
11
{
12
- public SearchFacade(WixSearchTuple searchTuple, IntermediateTuple searchSpecificTuple)
12
+ public LegacySearchFacade(WixSearchTuple searchTuple, IntermediateTuple searchSpecificTuple)
13
{
14
this.SearchTuple = searchTuple;
15
this.SearchSpecificTuple = searchSpecificTuple;
16
}
17
18
- public WixSearchTuple SearchTuple { get; }
19
-
18
public IntermediateTuple SearchSpecificTuple { get; }
19
20
/// <summary>
21
/// Generates Burn manifest and ParameterInfo-style markup a search.
22
/// </summary>
23
/// <param name="writer"></param>
26
- public void WriteXml(XmlTextWriter writer)
24
+ public override void WriteXml(XmlTextWriter writer)
25
{
26
switch (this.SearchSpecificTuple)
27
{
30
- case WixComponentSearchTuple tuple:
31
- this.WriteComponentSearchXml(writer, tuple);
32
- break;
33
- case WixFileSearchTuple tuple:
34
- this.WriteFileSearchXml(writer, tuple);
35
- break;
36
- case WixProductSearchTuple tuple:
37
- this.WriteProductSearchXml(writer, tuple);
38
- break;
39
- case WixRegistrySearchTuple tuple:
40
- this.WriteRegistrySearchXml(writer, tuple);
41
- break;
42
- }
43
- }
44
-
45
- private void WriteCommonAttributes(XmlTextWriter writer)
46
- {
47
- writer.WriteAttributeString("Id", this.SearchTuple.Id.Id);
48
- writer.WriteAttributeString("Variable", this.SearchTuple.Variable);
49
- if (!String.IsNullOrEmpty(this.SearchTuple.Condition))
50
- {
51
- writer.WriteAttributeString("Condition", this.SearchTuple.Condition);
28
+ case WixComponentSearchTuple tuple:
29
+ this.WriteComponentSearchXml(writer, tuple);
30
+ break;
31
+ case WixFileSearchTuple tuple:
32
+ this.WriteFileSearchXml(writer, tuple);
33
+ break;
34
+ case WixProductSearchTuple tuple:
35
+ this.WriteProductSearchXml(writer, tuple);
36
+ break;
37
+ case WixRegistrySearchTuple tuple:
38
+ this.WriteRegistrySearchXml(writer, tuple);
39
+ break;
40
}
41
}
42
@@ -56,7 +44,7 @@ namespace WixToolset.Core.Burn
44
{
45
writer.WriteStartElement("MsiComponentSearch");
46
59
- this.WriteCommonAttributes(writer);
47
+ base.WriteXml(writer);
48
49
writer.WriteAttributeString("ComponentId", searchTuple.Guid);
50
@@ -85,7 +73,7 @@ namespace WixToolset.Core.Burn
73
{
74
writer.WriteStartElement((0 == (searchTuple.Attributes & WixFileSearchAttributes.IsDirectory)) ? "FileSearch" : "DirectorySearch");
75
88
- this.WriteCommonAttributes(writer);
76
+ base.WriteXml(writer);
77
78
writer.WriteAttributeString("Path", searchTuple.Path);
79
if (WixFileSearchAttributes.WantExists == (searchTuple.Attributes & WixFileSearchAttributes.WantExists))
@@ -108,7 +96,7 @@ namespace WixToolset.Core.Burn
96
{
97
writer.WriteStartElement("MsiProductSearch");
98
111
- this.WriteCommonAttributes(writer);
99
+ base.WriteXml(writer);
100
101
if (0 != (tuple.Attributes & WixProductSearchAttributes.UpgradeCode))
102
{
@@ -143,22 +131,22 @@ namespace WixToolset.Core.Burn
131
{
132
writer.WriteStartElement("RegistrySearch");
133
146
- this.WriteCommonAttributes(writer);
134
+ base.WriteXml(writer);
135
136
switch (tuple.Root)
137
{
150
- case RegistryRootType.ClassesRoot:
151
- writer.WriteAttributeString("Root", "HKCR");
152
- break;
153
- case RegistryRootType.CurrentUser:
154
- writer.WriteAttributeString("Root", "HKCU");
155
- break;
156
- case RegistryRootType.LocalMachine:
157
- writer.WriteAttributeString("Root", "HKLM");
158
- break;
159
- case RegistryRootType.Users:
160
- writer.WriteAttributeString("Root", "HKU");
161
- break;
138
+ case RegistryRootType.ClassesRoot:
139
+ writer.WriteAttributeString("Root", "HKCR");
140
+ break;
141
+ case RegistryRootType.CurrentUser:
142
+ writer.WriteAttributeString("Root", "HKCU");
143
+ break;
144
+ case RegistryRootType.LocalMachine:
145
+ writer.WriteAttributeString("Root", "HKLM");
146
+ break;
147
+ case RegistryRootType.Users:
148
+ writer.WriteAttributeString("Root", "HKU");
149
+ break;
150
}
151
152
writer.WriteAttributeString("Key", tuple.Key);
src/WixToolset.Core.Burn/Bundles/BurnCommon.cs
+7
-1
@@ -22,6 +22,12 @@ namespace WixToolset.Core.Burn.Bundles
22
public const string BurnUXContainerPayloadIdFormat = "p{0}";
23
public const string BurnAttachedContainerEmbeddedIdFormat = "a{0}";
24
25
+ public const string BADataFileName = "BootstrapperApplicationData.xml";
26
+ public const string BADataNamespace = "http://wixtoolset.org/schemas/v4/BootstrapperApplicationData";
27
+
28
+ public const string BundleExtensionDataFileName = "BundleExtensionData.xml";
29
+ public const string BundleExtensionDataNamespace = "http://wixtoolset.org/schemas/v4/BundleExtensionData";
30
+
31
// See WinNT.h for details about the PE format, including the
32
// structure and offsets for IMAGE_DOS_HEADER, IMAGE_NT_HEADERS32,
33
// IMAGE_FILE_HEADER, etc.
@@ -167,7 +173,7 @@ namespace WixToolset.Core.Burn.Bundles
173
/// <returns>True if initialized.</returns>
174
protected bool Initialize(BinaryReader reader)
175
{
170
- if (!GetWixburnSectionInfo(reader))
176
+ if (!this.GetWixburnSectionInfo(reader))
177
{
178
return false;
179
}
src/WixToolset.Core.Burn/Bundles/CreateBootstrapperApplicationManifestCommand.cs
+3
-3
@@ -57,7 +57,7 @@ namespace WixToolset.Core.Burn.Bundles
57
{
58
writer.Formatting = Formatting.Indented;
59
writer.WriteStartDocument();
60
- writer.WriteStartElement("BootstrapperApplicationData", "http://wixtoolset.org/schemas/v4/BootstrapperApplicationData");
60
+ writer.WriteStartElement("BootstrapperApplicationData", BurnCommon.BADataNamespace);
61
62
this.WriteBundleInfo(writer);
63
@@ -247,11 +247,11 @@ namespace WixToolset.Core.Burn.Bundles
247
248
private WixBundlePayloadTuple CreateBootstrapperApplicationManifestPayloadRow(string baManifestPath)
249
{
250
- var generatedId = Common.GenerateIdentifier("ux", "BootstrapperApplicationData.xml");
250
+ var generatedId = Common.GenerateIdentifier("ux", BurnCommon.BADataFileName);
251
252
var tuple = new WixBundlePayloadTuple(this.BundleTuple.SourceLineNumbers, new Identifier(AccessModifier.Private, generatedId))
253
{
254
- Name = "BootstrapperApplicationData.xml",
254
+ Name = BurnCommon.BADataFileName,
255
SourceFile = new IntermediateFieldPathValue { Path = baManifestPath },
256
Compressed = true,
257
UnresolvedSourceFile = baManifestPath,
src/WixToolset.Core.Burn/Bundles/CreateBundleExtensionManifestCommand.cs
new
+149
@@ -0,0 +1,149 @@
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
+
3
+namespace WixToolset.Core.Burn.Bundles
4
+{
5
+ using System;
6
+ using System.Collections.Generic;
7
+ using System.Diagnostics;
8
+ using System.Globalization;
9
+ using System.IO;
10
+ using System.Linq;
11
+ using System.Text;
12
+ using System.Xml;
13
+ using WixToolset.Data;
14
+ using WixToolset.Data.Burn;
15
+ using WixToolset.Data.Tuples;
16
+
17
+ internal class CreateBundleExtensionManifestCommand
18
+ {
19
+ public CreateBundleExtensionManifestCommand(IntermediateSection section, WixBundleTuple bundleTuple, IDictionary<string, IList<IntermediateTuple>> extensionSearchTuplesByExtensionId, int lastUXPayloadIndex, string intermediateFolder)
20
+ {
21
+ this.Section = section;
22
+ this.BundleTuple = bundleTuple;
23
+ this.ExtensionSearchTuplesByExtensionId = extensionSearchTuplesByExtensionId;
24
+ this.LastUXPayloadIndex = lastUXPayloadIndex;
25
+ this.IntermediateFolder = intermediateFolder;
26
+ }
27
+
28
+ private IntermediateSection Section { get; }
29
+
30
+ private WixBundleTuple BundleTuple { get; }
31
+
32
+ private IDictionary<string, IList<IntermediateTuple>> ExtensionSearchTuplesByExtensionId { get; }
33
+
34
+ private int LastUXPayloadIndex { get; }
35
+
36
+ private string IntermediateFolder { get; }
37
+
38
+ public WixBundlePayloadTuple BundleExtensionManifestPayloadRow { get; private set; }
39
+
40
+ public void Execute()
41
+ {
42
+ var bextManifestPath = this.CreateBundleExtensionManifest();
43
+
44
+ this.BundleExtensionManifestPayloadRow = this.CreateBundleExtensionManifestPayloadRow(bextManifestPath);
45
+ }
46
+
47
+ private string CreateBundleExtensionManifest()
48
+ {
49
+ var path = Path.Combine(this.IntermediateFolder, "wix-bextdata.xml");
50
+
51
+ Directory.CreateDirectory(Path.GetDirectoryName(path));
52
+
53
+ using (var writer = new XmlTextWriter(path, Encoding.Unicode))
54
+ {
55
+ writer.Formatting = Formatting.Indented;
56
+ writer.WriteStartDocument();
57
+ writer.WriteStartElement("BundleExtensionData", BurnCommon.BundleExtensionDataNamespace);
58
+
59
+ foreach (var kvp in this.ExtensionSearchTuplesByExtensionId)
60
+ {
61
+ this.WriteExtension(writer, kvp.Key, kvp.Value);
62
+ }
63
+
64
+ writer.WriteEndElement();
65
+ writer.WriteEndDocument();
66
+ }
67
+
68
+ return path;
69
+ }
70
+
71
+ private void WriteExtension(XmlTextWriter writer, string extensionId, IEnumerable<IntermediateTuple> tuples)
72
+ {
73
+ writer.WriteStartElement("BundleExtension");
74
+
75
+ writer.WriteAttributeString("Id", extensionId);
76
+
77
+ this.WriteBundleExtensionDataTuples(writer, tuples);
78
+
79
+ writer.WriteEndElement();
80
+ }
81
+
82
+ private void WriteBundleExtensionDataTuples(XmlTextWriter writer, IEnumerable<IntermediateTuple> tuples)
83
+ {
84
+ var dataTuplesGroupedByDefinitionName = tuples.GroupBy(t => t.Definition);
85
+
86
+ foreach (var group in dataTuplesGroupedByDefinitionName)
87
+ {
88
+ var definition = group.Key;
89
+
90
+ // We simply assert that the table (and field) name is valid, because
91
+ // this is up to the extension developer to get right. An author will
92
+ // only affect the attribute value, and that will get properly escaped.
93
+#if DEBUG
94
+ Debug.Assert(Common.IsIdentifier(definition.Name));
95
+ foreach (var fieldDef in definition.FieldDefinitions)
96
+ {
97
+ Debug.Assert(Common.IsIdentifier(fieldDef.Name));
98
+ }
99
+#endif // DEBUG
100
+
101
+ foreach (var tuple in group)
102
+ {
103
+ writer.WriteStartElement(definition.Name);
104
+
105
+ if (tuple.Id != null)
106
+ {
107
+ writer.WriteAttributeString("Id", tuple.Id.Id);
108
+ }
109
+
110
+ foreach (var field in tuple.Fields)
111
+ {
112
+ if (!field.IsNull())
113
+ {
114
+ writer.WriteAttributeString(field.Definition.Name, field.AsString());
115
+ }
116
+ }
117
+
118
+ writer.WriteEndElement();
119
+ }
120
+ }
121
+ }
122
+
123
+ private WixBundlePayloadTuple CreateBundleExtensionManifestPayloadRow(string bextManifestPath)
124
+ {
125
+ var generatedId = Common.GenerateIdentifier("ux", BurnCommon.BundleExtensionDataFileName);
126
+
127
+ var tuple = new WixBundlePayloadTuple(this.BundleTuple.SourceLineNumbers, new Identifier(AccessModifier.Private, generatedId))
128
+ {
129
+ Name = BurnCommon.BundleExtensionDataFileName,
130
+ SourceFile = new IntermediateFieldPathValue { Path = bextManifestPath },
131
+ Compressed = true,
132
+ UnresolvedSourceFile = bextManifestPath,
133
+ ContainerRef = BurnConstants.BurnUXContainerName,
134
+ EmbeddedId = String.Format(CultureInfo.InvariantCulture, BurnCommon.BurnUXContainerEmbeddedIdFormat, this.LastUXPayloadIndex),
135
+ Packaging = PackagingType.Embedded,
136
+ };
137
+
138
+ var fileInfo = new FileInfo(bextManifestPath);
139
+
140
+ tuple.FileSize = (int)fileInfo.Length;
141
+
142
+ tuple.Hash = BundleHashAlgorithm.Hash(fileInfo);
143
+
144
+ this.Section.Tuples.Add(tuple);
145
+
146
+ return tuple;
147
+ }
148
+ }
149
+}
src/WixToolset.Core.Burn/Bundles/CreateBurnManifestCommand.cs
+2
-2
@@ -18,7 +18,7 @@ namespace WixToolset.Core.Burn.Bundles
18
19
internal class CreateBurnManifestCommand
20
{
21
- public CreateBurnManifestCommand(IMessaging messaging, IEnumerable<IBurnBackendExtension> backendExtensions, string executableName, IntermediateSection section, WixBundleTuple bundleTuple, IEnumerable<WixBundleContainerTuple> containers, WixChainTuple chainTuple, IEnumerable<PackageFacade> orderedPackages, IEnumerable<WixBundleRollbackBoundaryTuple> boundaries, IEnumerable<WixBundlePayloadTuple> uxPayloads, Dictionary<string, WixBundlePayloadTuple> allPayloadsById, IEnumerable<SearchFacade> orderedSearches, IEnumerable<WixBundleCatalogTuple> catalogs, string intermediateFolder)
21
+ public CreateBurnManifestCommand(IMessaging messaging, IEnumerable<IBurnBackendExtension> backendExtensions, string executableName, IntermediateSection section, WixBundleTuple bundleTuple, IEnumerable<WixBundleContainerTuple> containers, WixChainTuple chainTuple, IEnumerable<PackageFacade> orderedPackages, IEnumerable<WixBundleRollbackBoundaryTuple> boundaries, IEnumerable<WixBundlePayloadTuple> uxPayloads, Dictionary<string, WixBundlePayloadTuple> allPayloadsById, IEnumerable<ISearchFacade> orderedSearches, IEnumerable<WixBundleCatalogTuple> catalogs, string intermediateFolder)
22
{
23
this.Messaging = messaging;
24
this.BackendExtensions = backendExtensions;
@@ -54,7 +54,7 @@ namespace WixToolset.Core.Burn.Bundles
54
55
private IEnumerable<PackageFacade> OrderedPackages { get; }
56
57
- private IEnumerable<SearchFacade> OrderedSearches { get; }
57
+ private IEnumerable<ISearchFacade> OrderedSearches { get; }
58
59
private Dictionary<string, WixBundlePayloadTuple> Payloads { get; }
60
src/WixToolset.Core.Burn/Bundles/OrderSearchesCommand.cs
new
+71
@@ -0,0 +1,71 @@
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
+
3
+namespace WixToolset.Core.Burn.Bundles
4
+{
5
+ using System.Collections.Generic;
6
+ using System.Linq;
7
+ using WixToolset.Data;
8
+ using WixToolset.Data.Burn;
9
+ using WixToolset.Data.Tuples;
10
+ using WixToolset.Extensibility.Services;
11
+
12
+ internal class OrderSearchesCommand
13
+ {
14
+ public OrderSearchesCommand(IMessaging messaging, IntermediateSection section)
15
+ {
16
+ this.Messaging = messaging;
17
+ this.Section = section;
18
+ }
19
+
20
+ private IMessaging Messaging { get; }
21
+
22
+ private IntermediateSection Section { get; }
23
+
24
+ public IDictionary<string, IList<IntermediateTuple>> ExtensionSearchTuplesByExtensionId { get; private set; }
25
+
26
+ public IList<ISearchFacade> OrderedSearchFacades { get; private set; }
27
+
28
+ public void Execute()
29
+ {
30
+ // TODO: Although the WixSearch tables are defined in the Util extension,
31
+ // the Bundle Binder has to know all about them. We hope to revisit all
32
+ // of this in the 4.0 timeframe.
33
+ var legacySearchesById = this.Section.Tuples
34
+ .Where(t => t.Definition.Type == TupleDefinitionType.WixComponentSearch ||
35
+ t.Definition.Type == TupleDefinitionType.WixFileSearch ||
36
+ t.Definition.Type == TupleDefinitionType.WixProductSearch ||
37
+ t.Definition.Type == TupleDefinitionType.WixRegistrySearch)
38
+ .ToDictionary(t => t.Id.Id);
39
+ var extensionSearchesById = this.Section.Tuples
40
+ .Where(t => t.Definition.HasTag(BurnConstants.BundleExtensionSearchTupleDefinitionTag))
41
+ .ToDictionary(t => t.Id.Id);
42
+ var searchTuples = this.Section.Tuples.OfType<WixSearchTuple>().ToList();
43
+
44
+ this.ExtensionSearchTuplesByExtensionId = new Dictionary<string, IList<IntermediateTuple>>();
45
+ this.OrderedSearchFacades = new List<ISearchFacade>(legacySearchesById.Keys.Count + extensionSearchesById.Keys.Count);
46
+
47
+ foreach (var searchTuple in searchTuples)
48
+ {
49
+ if (legacySearchesById.TryGetValue(searchTuple.Id.Id, out var specificSearchTuple))
50
+ {
51
+ this.OrderedSearchFacades.Add(new LegacySearchFacade(searchTuple, specificSearchTuple));
52
+ }
53
+ else if (extensionSearchesById.TryGetValue(searchTuple.Id.Id, out var extensionSearchTuple))
54
+ {
55
+ this.OrderedSearchFacades.Add(new ExtensionSearchFacade(searchTuple));
56
+
57
+ if (!this.ExtensionSearchTuplesByExtensionId.TryGetValue(searchTuple.BundleExtensionRef, out var extensionSearchTuples))
58
+ {
59
+ extensionSearchTuples = new List<IntermediateTuple>();
60
+ this.ExtensionSearchTuplesByExtensionId[searchTuple.BundleExtensionRef] = extensionSearchTuples;
61
+ }
62
+ extensionSearchTuples.Add(extensionSearchTuple);
63
+ }
64
+ else
65
+ {
66
+ this.Messaging.Write(ErrorMessages.MissingBundleSearch(searchTuple.SourceLineNumbers, searchTuple.Id.Id));
67
+ }
68
+ }
69
+ }
70
+ }
71
+}
src/WixToolset.Core.Burn/ISearchFacade.cs
new
+15
@@ -0,0 +1,15 @@
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
+
3
+namespace WixToolset.Core.Burn
4
+{
5
+ using System.Xml;
6
+
7
+ internal interface ISearchFacade
8
+ {
9
+ /// <summary>
10
+ /// Writes the search to the Burn manifest.
11
+ /// </summary>
12
+ /// <param name="writer"></param>
13
+ void WriteXml(XmlTextWriter writer);
14
+ }
15
+}
src/WixToolset.Core.TestPackage/BundleExtractor.cs
+34
@@ -22,11 +22,31 @@ namespace WixToolset.Core.TestPackage
22
{
23
result.ManifestDocument = LoadBurnManifest(destinationFolderPath);
24
result.ManifestNamespaceManager = GetBurnNamespaceManager(result.ManifestDocument, "burn");
25
+
26
+ result.BADataDocument = LoadBAData(destinationFolderPath);
27
+ result.BADataNamespaceManager = GetBADataNamespaceManager(result.BADataDocument, "ba");
28
+
29
+ result.BundleExtensionDataDocument = LoadBundleExtensionData(destinationFolderPath);
30
+ result.BundleExtensionDataNamespaceManager = GetBundleExtensionDataNamespaceManager(result.BundleExtensionDataDocument, "be");
31
}
32
33
return result;
34
}
35
36
+ public static XmlNamespaceManager GetBADataNamespaceManager(XmlDocument document, string prefix)
37
+ {
38
+ var namespaceManager = new XmlNamespaceManager(document.NameTable);
39
+ namespaceManager.AddNamespace(prefix, BurnCommon.BADataNamespace);
40
+ return namespaceManager;
41
+ }
42
+
43
+ public static XmlNamespaceManager GetBundleExtensionDataNamespaceManager(XmlDocument document, string prefix)
44
+ {
45
+ var namespaceManager = new XmlNamespaceManager(document.NameTable);
46
+ namespaceManager.AddNamespace(prefix, BurnCommon.BundleExtensionDataNamespace);
47
+ return namespaceManager;
48
+ }
49
+
50
public static XmlNamespaceManager GetBurnNamespaceManager(XmlDocument document, string prefix)
51
{
52
var namespaceManager = new XmlNamespaceManager(document.NameTable);
@@ -34,6 +54,20 @@ namespace WixToolset.Core.TestPackage
54
return namespaceManager;
55
}
56
57
+ public static XmlDocument LoadBAData(string baFolderPath)
58
+ {
59
+ var document = new XmlDocument();
60
+ document.Load(Path.Combine(baFolderPath, BurnCommon.BADataFileName));
61
+ return document;
62
+ }
63
+
64
+ public static XmlDocument LoadBundleExtensionData(string baFolderPath)
65
+ {
66
+ var document = new XmlDocument();
67
+ document.Load(Path.Combine(baFolderPath, BurnCommon.BundleExtensionDataFileName));
68
+ return document;
69
+ }
70
+
71
public static XmlDocument LoadBurnManifest(string baFolderPath)
72
{
73
var document = new XmlDocument();
src/WixToolset.Core.TestPackage/ExtractBAContainerResult.cs
+32
@@ -8,6 +8,10 @@ namespace WixToolset.Core.TestPackage
8
9
public class ExtractBAContainerResult
10
{
11
+ public XmlDocument BundleExtensionDataDocument { get; set; }
12
+ public XmlNamespaceManager BundleExtensionDataNamespaceManager { get; set; }
13
+ public XmlDocument BADataDocument { get; set; }
14
+ public XmlNamespaceManager BADataNamespaceManager { get; set; }
15
public XmlDocument ManifestDocument { get; set; }
16
public XmlNamespaceManager ManifestNamespaceManager { get; set; }
17
public bool Success { get; set; }
@@ -26,6 +30,34 @@ namespace WixToolset.Core.TestPackage
30
return Path.Combine(extractedBAContainerFolderPath, relativeBAPath);
31
}
32
33
+ public string GetBundleExtensionFilePath(string extractedBAContainerFolderPath, string extensionId)
34
+ {
35
+ var uxPayloads = this.SelectManifestNodes($"/burn:BurnManifest/burn:UX/burn:Payload[@Id='{extensionId}']");
36
+ var bextPayload = uxPayloads[0];
37
+ var relativeBextPath = bextPayload.Attributes["FilePath"].Value;
38
+ return Path.Combine(extractedBAContainerFolderPath, relativeBextPath);
39
+ }
40
+
41
+ /// <summary>
42
+ ///
43
+ /// </summary>
44
+ /// <param name="xpath">elements must have the 'ba' prefix</param>
45
+ /// <returns></returns>
46
+ public XmlNodeList SelectBADataNodes(string xpath)
47
+ {
48
+ return this.BADataDocument.SelectNodes(xpath, this.BADataNamespaceManager);
49
+ }
50
+
51
+ /// <summary>
52
+ ///
53
+ /// </summary>
54
+ /// <param name="xpath">elements must have the 'be' prefix</param>
55
+ /// <returns></returns>
56
+ public XmlNodeList SelectBundleExtensionDataNodes(string xpath)
57
+ {
58
+ return this.BundleExtensionDataDocument.SelectNodes(xpath, this.BundleExtensionDataNamespaceManager);
59
+ }
60
+
61
/// <summary>
62
///
63
/// </summary>
src/WixToolset.Core/CompilerCore.cs
+5
@@ -1026,6 +1026,11 @@ namespace WixToolset.Core
1026
return this.parseHelper.CreateDirectoryTuple(this.ActiveSection, sourceLineNumbers, id, parentId, name, this.activeSectionInlinedDirectoryIds, shortName, sourceName, shortSourceName);
1027
}
1028
1029
+ public void CreateWixSearchTuple(SourceLineNumber sourceLineNumbers, string elementName, Identifier id, string variable, string condition, string after)
1030
+ {
1031
+ this.parseHelper.CreateWixSearchTuple(this.ActiveSection, sourceLineNumbers, elementName, id, variable, condition, after, null);
1032
+ }
1033
+
1034
/// <summary>
1035
/// Gets the attribute value as inline directory syntax.
1036
/// </summary>
src/WixToolset.Core/ExtensibilityServices/ParseHelper.cs
+37
@@ -260,6 +260,43 @@ namespace WixToolset.Core.ExtensibilityServices
260
section.Tuples.Add(tuple);
261
}
262
263
+ public void CreateWixSearchTuple(IntermediateSection section, SourceLineNumber sourceLineNumbers, string elementName, Identifier id, string variable, string condition, string after, string bundleExtensionId)
264
+ {
265
+ // TODO: verify variable is not a standard bundle variable
266
+ if (variable == null)
267
+ {
268
+ this.Messaging.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, elementName, "Variable"));
269
+ }
270
+
271
+ section.Tuples.Add(new WixSearchTuple(sourceLineNumbers, id)
272
+ {
273
+ Variable = variable,
274
+ Condition = condition,
275
+ BundleExtensionRef = bundleExtensionId,
276
+ });
277
+
278
+ if (after != null)
279
+ {
280
+ this.CreateSimpleReference(section, sourceLineNumbers, "WixSearch", after);
281
+ // TODO: We're currently defaulting to "always run after", which we will need to change...
282
+ this.CreateWixSearchRelationTuple(section, sourceLineNumbers, id, after, 2);
283
+ }
284
+
285
+ if (!String.IsNullOrEmpty(bundleExtensionId))
286
+ {
287
+ this.CreateSimpleReference(section, sourceLineNumbers, "WixBundleExtension", bundleExtensionId);
288
+ }
289
+ }
290
+
291
+ public void CreateWixSearchRelationTuple(IntermediateSection section, SourceLineNumber sourceLineNumbers, Identifier id, string parentId, int attributes)
292
+ {
293
+ section.Tuples.Add(new WixSearchRelationTuple(sourceLineNumbers, id)
294
+ {
295
+ ParentSearchRef = parentId,
296
+ Attributes = attributes,
297
+ });
298
+ }
299
+
300
[Obsolete]
301
public IntermediateTuple CreateRow(IntermediateSection section, SourceLineNumber sourceLineNumbers, string tableName, Identifier identifier = null)
302
{
src/test/Example.Extension/Data/example.wxs
+3
@@ -8,4 +8,7 @@
8
<Fragment>
9
<BootstrapperApplication Id="fakeba" SourceFile="example.txt" />
10
</Fragment>
11
+ <Fragment>
12
+ <BundleExtension Id="ExampleBundleExtension" SourceFile="example.txt" />
13
+ </Fragment>
14
</Wix>
src/test/Example.Extension/ExampleCompilerExtension.cs
+104
@@ -11,6 +11,7 @@ namespace Example.Extension
11
internal class ExampleCompilerExtension : BaseCompilerExtension
12
{
13
public override XNamespace Namespace => "http://www.example.com/scheams/v1/wxs";
14
+ public string BundleExtensionId => "ExampleBundleExtension";
15
16
public override void ParseElement(Intermediate intermediate, IntermediateSection section, XElement parentElement, XElement element, IDictionary<string, string> context)
17
{
@@ -18,6 +19,20 @@ namespace Example.Extension
19
20
switch (parentElement.Name.LocalName)
21
{
22
+ case "Bundle":
23
+ case "Fragment":
24
+ switch (element.Name.LocalName)
25
+ {
26
+ case "ExampleSearch":
27
+ this.ParseExampleSearchElement(intermediate, section, element);
28
+ processed = true;
29
+ break;
30
+ case "ExampleSearchRef":
31
+ this.ParseExampleSearchRefElement(intermediate, section, element);
32
+ processed = true;
33
+ break;
34
+ }
35
+ break;
36
case "Component":
37
switch (element.Name.LocalName)
38
{
@@ -77,5 +92,94 @@ namespace Example.Extension
92
tuple.Set(1, value);
93
}
94
}
95
+
96
+ private void ParseExampleSearchElement(Intermediate intermediate, IntermediateSection section, XElement element)
97
+ {
98
+ var sourceLineNumbers = this.ParseHelper.GetSourceLineNumbers(element);
99
+ Identifier id = null;
100
+ string searchFor = null;
101
+ string variable = null;
102
+ string condition = null;
103
+ string after = null;
104
+
105
+ foreach (var attrib in element.Attributes())
106
+ {
107
+ if (String.IsNullOrEmpty(attrib.Name.NamespaceName) || this.Namespace == attrib.Name.Namespace)
108
+ {
109
+ switch (attrib.Name.LocalName)
110
+ {
111
+ case "Id":
112
+ id = this.ParseHelper.GetAttributeIdentifier(sourceLineNumbers, attrib);
113
+ break;
114
+ case "Variable":
115
+ variable = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
116
+ break;
117
+ case "Condition":
118
+ condition = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
119
+ break;
120
+ case "After":
121
+ after = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
122
+ break;
123
+ case "SearchFor":
124
+ searchFor = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
125
+ break;
126
+
127
+ default:
128
+ this.ParseHelper.UnexpectedAttribute(element, attrib);
129
+ break;
130
+ }
131
+ }
132
+ else
133
+ {
134
+ this.ParseAttribute(intermediate, section, element, attrib, null);
135
+ }
136
+ }
137
+
138
+ if (null == id)
139
+ {
140
+ this.Messaging.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, element.Name.LocalName, "Id"));
141
+ }
142
+
143
+ if (!this.Messaging.EncounteredError)
144
+ {
145
+ this.ParseHelper.CreateWixSearchTuple(section, sourceLineNumbers, element.Name.LocalName, id, variable, condition, after, this.BundleExtensionId);
146
+ }
147
+
148
+ if (!this.Messaging.EncounteredError)
149
+ {
150
+
151
+ var tuple = new ExampleSearchTuple(sourceLineNumbers, id);
152
+ section.Tuples.Add(tuple);
153
+ tuple.SearchFor = searchFor;
154
+ }
155
+ }
156
+
157
+ private void ParseExampleSearchRefElement(Intermediate intermediate, IntermediateSection section, XElement element)
158
+ {
159
+ var sourceLineNumbers = this.ParseHelper.GetSourceLineNumbers(element);
160
+
161
+ foreach (var attrib in element.Attributes())
162
+ {
163
+ if (String.IsNullOrEmpty(attrib.Name.NamespaceName) || this.Namespace == attrib.Name.Namespace)
164
+ {
165
+ switch (attrib.Name.LocalName)
166
+ {
167
+ case "Id":
168
+ var refId = this.ParseHelper.GetAttributeIdentifierValue(sourceLineNumbers, attrib);
169
+ this.ParseHelper.CreateSimpleReference(section, sourceLineNumbers, "ExampleSearch", refId);
170
+ break;
171
+ default:
172
+ this.ParseHelper.UnexpectedAttribute(element, attrib);
173
+ break;
174
+ }
175
+ }
176
+ else
177
+ {
178
+ this.ParseHelper.ParseExtensionAttribute(this.Context.Extensions, intermediate, section, element, attrib);
179
+ }
180
+ }
181
+
182
+ this.ParseHelper.ParseForExtensionElements(this.Context.Extensions, intermediate, section, element);
183
+ }
184
}
185
}
src/test/Example.Extension/ExampleExtensionData.cs
+5
-1
@@ -1,4 +1,4 @@
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.
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
3
namespace Example.Extension
4
{
@@ -22,6 +22,10 @@ namespace Example.Extension
22
tupleDefinition = ExampleTupleDefinitions.Example;
23
break;
24
25
+ case "ExampleSearch":
26
+ tupleDefinition = ExampleTupleDefinitions.ExampleSearch;
27
+ break;
28
+
29
default:
30
tupleDefinition = null;
31
break;
src/test/Example.Extension/ExampleSearchTuple.cs
new
+31
@@ -0,0 +1,31 @@
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
+
3
+namespace Example.Extension
4
+{
5
+ using WixToolset.Data;
6
+
7
+ public enum ExampleSearchTupleFields
8
+ {
9
+ Example,
10
+ SearchFor,
11
+ }
12
+
13
+ public class ExampleSearchTuple : IntermediateTuple
14
+ {
15
+ public ExampleSearchTuple() : base(ExampleTupleDefinitions.ExampleSearch, null, null)
16
+ {
17
+ }
18
+
19
+ public ExampleSearchTuple(SourceLineNumber sourceLineNumber, Identifier id = null) : base(ExampleTupleDefinitions.ExampleSearch, sourceLineNumber, id)
20
+ {
21
+ }
22
+
23
+ public IntermediateField this[ExampleTupleFields index] => this.Fields[(int)index];
24
+
25
+ public string SearchFor
26
+ {
27
+ get => this.Fields[(int)ExampleSearchTupleFields.SearchFor]?.AsString();
28
+ set => this.Set((int)ExampleSearchTupleFields.SearchFor, value);
29
+ }
30
+ }
31
+}
src/test/Example.Extension/ExampleTupleDefinitions.cs
+16
-1
@@ -1,8 +1,9 @@
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.
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
3
namespace Example.Extension
4
{
5
using WixToolset.Data;
6
+ using WixToolset.Data.Burn;
7
8
public static class ExampleTupleDefinitions
9
{
@@ -16,5 +17,19 @@ namespace Example.Extension
17
new IntermediateFieldDefinition(nameof(ExampleTupleFields.Value), IntermediateFieldType.String),
18
},
19
typeof(ExampleTuple));
20
+
21
+ public static readonly IntermediateTupleDefinition ExampleSearch = new IntermediateTupleDefinition(
22
+ nameof(ExampleSearch),
23
+ new[]
24
+ {
25
+ new IntermediateFieldDefinition(nameof(ExampleTupleFields.Example), IntermediateFieldType.String),
26
+ new IntermediateFieldDefinition(nameof(ExampleSearchTupleFields.SearchFor), IntermediateFieldType.String),
27
+ },
28
+ typeof(ExampleSearchTuple));
29
+
30
+ static ExampleTupleDefinitions()
31
+ {
32
+ ExampleSearch.AddTag(BurnConstants.BundleExtensionSearchTupleDefinitionTag);
33
+ }
34
}
35
}
src/test/WixToolsetTest.CoreIntegration/BundleManifestFixture.cs
+56
@@ -2,8 +2,10 @@
2
3
namespace WixToolsetTest.CoreIntegration
4
{
5
+ using System;
6
using System.Collections.Generic;
7
using System.IO;
8
+ using Example.Extension;
9
using WixBuildTools.TestSupport;
10
using WixToolset.Core.TestPackage;
11
using Xunit;
@@ -57,5 +59,59 @@ namespace WixToolsetTest.CoreIntegration
59
Assert.Equal("<Payload Id='ExampleBext' FilePath='fakebext.dll' FileSize='*' Hash='*' Packaging='embedded' SourcePath='*' />", bundleExtensionPayloads[0].GetTestXml(ignored));
60
}
61
}
62
+
63
+ [Fact]
64
+ public void PopulatesManifestWithBundleExtensionSearches()
65
+ {
66
+ var burnStubPath = TestData.Get(@"TestData\.Data\burn.exe");
67
+ var extensionPath = Path.GetFullPath(new Uri(typeof(ExampleExtensionFactory).Assembly.CodeBase).LocalPath);
68
+ var folder = TestData.Get(@"TestData");
69
+
70
+ using (var fs = new DisposableFileSystem())
71
+ {
72
+ var baseFolder = fs.GetFolder();
73
+ var intermediateFolder = Path.Combine(baseFolder, "obj");
74
+ var bundlePath = Path.Combine(baseFolder, @"bin\test.exe");
75
+ var baFolderPath = Path.Combine(baseFolder, "ba");
76
+ var extractFolderPath = Path.Combine(baseFolder, "extract");
77
+
78
+ var result = WixRunner.Execute(new[]
79
+ {
80
+ "build",
81
+ Path.Combine(folder, "BundleExtension", "BundleExtensionSearches.wxs"),
82
+ Path.Combine(folder, "BundleExtension", "BundleWithSearches.wxs"),
83
+ Path.Combine(folder, "BundleWithPackageGroupRef", "MinimalPackageGroup.wxs"),
84
+ Path.Combine(folder, "BundleWithPackageGroupRef", "Bundle.wxs"),
85
+ "-ext", extensionPath,
86
+ "-bindpath", Path.Combine(folder, "SimpleBundle", "data"),
87
+ "-intermediateFolder", intermediateFolder,
88
+ "-burnStub", burnStubPath,
89
+ "-o", bundlePath
90
+ });
91
+
92
+ result.AssertSuccess();
93
+
94
+ Assert.True(File.Exists(bundlePath));
95
+
96
+ var extractResult = BundleExtractor.ExtractBAContainer(null, bundlePath, baFolderPath, extractFolderPath);
97
+ extractResult.AssertSuccess();
98
+
99
+ var bundleExtensions = extractResult.SelectManifestNodes("/burn:BurnManifest/burn:BundleExtension");
100
+ Assert.Equal(1, bundleExtensions.Count);
101
+ Assert.Equal("<BundleExtension Id='ExampleBundleExtension' EntryPayloadId='ExampleBundleExtension' />", bundleExtensions[0].GetTestXml());
102
+
103
+ var extensionSearches = extractResult.SelectManifestNodes("/burn:BurnManifest/burn:ExtensionSearch");
104
+ Assert.Equal(2, extensionSearches.Count);
105
+ Assert.Equal("<ExtensionSearch Id='ExampleSearchBar' Variable='SearchBar' Condition='WixBundleInstalled' ExtensionId='ExampleBundleExtension' />", extensionSearches[0].GetTestXml());
106
+ Assert.Equal("<ExtensionSearch Id='ExampleSearchFoo' Variable='SearchFoo' ExtensionId='ExampleBundleExtension' />", extensionSearches[1].GetTestXml());
107
+
108
+ var bundleExtensionDatas = extractResult.SelectBundleExtensionDataNodes("/be:BundleExtensionData/be:BundleExtension[@Id='ExampleBundleExtension']");
109
+ Assert.Equal(1, bundleExtensionDatas.Count);
110
+ Assert.Equal("<BundleExtension Id='ExampleBundleExtension'>" +
111
+ "<ExampleSearch Id='ExampleSearchBar' SearchFor='Bar' />" +
112
+ "<ExampleSearch Id='ExampleSearchFoo' SearchFor='Foo' />" +
113
+ "</BundleExtension>", bundleExtensionDatas[0].GetTestXml());
114
+ }
115
+ }
116
}
117
}
src/test/WixToolsetTest.CoreIntegration/TestData/BundleExtension/BundleExtensionSearches.wxs
new
+8
@@ -0,0 +1,8 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
3
+ xmlns:ex="http://www.example.com/scheams/v1/wxs">
4
+ <Fragment>
5
+ <ex:ExampleSearch Id="ExampleSearchBar" Variable="SearchBar" Condition="WixBundleInstalled" SearchFor="Bar" />
6
+ <ex:ExampleSearch Id="ExampleSearchFoo" Variable="SearchFoo" SearchFor="Foo" />
7
+ </Fragment>
8
+</Wix>
src/test/WixToolsetTest.CoreIntegration/TestData/BundleExtension/BundleWithSearches.wxs
new
+11
@@ -0,0 +1,11 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
3
+ xmlns:ex="http://www.example.com/scheams/v1/wxs">
4
+ <Fragment>
5
+ <PackageGroup Id="BundlePackages">
6
+ <PackageGroupRef Id="MinimalPackageGroup" />
7
+ </PackageGroup>
8
+
9
+ <ex:ExampleSearchRef Id="ExampleSearchFoo" />
10
+ </Fragment>
11
+</Wix>
src/test/WixToolsetTest.CoreIntegration/WixToolsetTest.CoreIntegration.csproj
+2
@@ -22,6 +22,8 @@
22
<Content Include="TestData\AppSearch\NestedDirSearchUnderRegSearch.msi" CopyToOutputDirectory="PreserveNewest" />
23
<Content Include="TestData\AppSearch\RegistrySearch.wxs" CopyToOutputDirectory="PreserveNewest" />
24
<Content Include="TestData\BundleExtension\BundleExtension.wxs" CopyToOutputDirectory="PreserveNewest" />
25
+ <Content Include="TestData\BundleExtension\BundleExtensionSearches.wxs" CopyToOutputDirectory="PreserveNewest" />
26
+ <Content Include="TestData\BundleExtension\BundleWithSearches.wxs" CopyToOutputDirectory="PreserveNewest" />
27
<Content Include="TestData\BundleExtension\SimpleBundleExtension.wxs" CopyToOutputDirectory="PreserveNewest" />
28
<Content Include="TestData\BundleWithPackageGroupRef\Bundle.wxs" CopyToOutputDirectory="PreserveNewest" />
29
<Content Include="TestData\BundleWithPackageGroupRef\MinimalPackageGroup.wxs" CopyToOutputDirectory="PreserveNewest" />