Order WixSearches in Core.
Sean Hall committed
Mar 27, 2020 at 15:49 UTC
bf435c69fd70f5140eddd99fe02d3dcdae75473a
3 files changed
+295
-10
src/WixToolset.Core.Burn/Bundles/OrderSearchesCommand.cs
+294
-9
@@ -2,7 +2,9 @@
2
3
namespace WixToolset.Core.Burn.Bundles
4
{
5
+ using System;
6
using System.Collections.Generic;
7
+ using System.Globalization;
8
using System.Linq;
9
using WixToolset.Data;
10
using WixToolset.Data.Burn;
@@ -26,6 +28,292 @@ namespace WixToolset.Core.Burn.Bundles
28
public IList<ISearchFacade> OrderedSearchFacades { get; private set; }
29
30
public void Execute()
31
+ {
32
+ this.ExtensionSearchTuplesByExtensionId = new Dictionary<string, IList<IntermediateTuple>>();
33
+ this.OrderedSearchFacades = new List<ISearchFacade>();
34
+
35
+ var searchRelationTuples = this.Section.Tuples.OfType<WixSearchRelationTuple>().ToList();
36
+ var searchTuples = this.Section.Tuples.OfType<WixSearchTuple>().ToList();
37
+ if (searchTuples.Count == 0)
38
+ {
39
+ // nothing to do!
40
+ return;
41
+ }
42
+
43
+ var tupleDictionary = searchTuples.ToDictionary(t => t.Id.Id);
44
+
45
+ var constraints = new Constraints();
46
+ if (searchRelationTuples.Count > 0)
47
+ {
48
+ // add relational info to our data...
49
+ foreach (var searchRelationTuple in searchRelationTuples)
50
+ {
51
+ constraints.AddConstraint(searchRelationTuple.Id.Id, searchRelationTuple.ParentSearchRef);
52
+ }
53
+ }
54
+
55
+ this.FindCircularReference(constraints);
56
+
57
+ if (this.Messaging.EncounteredError)
58
+ {
59
+ return;
60
+ }
61
+
62
+ this.FlattenDependentReferences(constraints);
63
+
64
+ // Reorder by topographical sort (http://en.wikipedia.org/wiki/Topological_sorting)
65
+ // We use a variation of Kahn (1962) algorithm as described in
66
+ // Wikipedia, with the additional criteria that start nodes are sorted
67
+ // lexicographically at each step to ensure a deterministic ordering
68
+ // based on 'after' dependencies and ID.
69
+ var sorter = new TopologicalSort();
70
+ var sortedIds = sorter.Sort(tupleDictionary.Keys, constraints);
71
+
72
+ // Now, create the search facades with the searches in order...
73
+ this.OrderSearches(sortedIds, tupleDictionary);
74
+ }
75
+
76
+ /// <summary>
77
+ /// A dictionary of constraints, mapping an id to a list of ids.
78
+ /// </summary>
79
+ private class Constraints : Dictionary<string, List<string>>
80
+ {
81
+ public void AddConstraint(string id, string afterId)
82
+ {
83
+ if (!this.ContainsKey(id))
84
+ {
85
+ this.Add(id, new List<string>());
86
+ }
87
+
88
+ // TODO: Show warning if a constraint is seen twice?
89
+ if (!this[id].Contains(afterId))
90
+ {
91
+ this[id].Add(afterId);
92
+ }
93
+ }
94
+
95
+ // TODO: Hide other Add methods?
96
+ }
97
+
98
+ /// <summary>
99
+ /// Finds circular references in the constraints.
100
+ /// </summary>
101
+ /// <param name="constraints">Constraints to check.</param>
102
+ /// <remarks>This is not particularly performant, but it works.</remarks>
103
+ private void FindCircularReference(Constraints constraints)
104
+ {
105
+ foreach (string id in constraints.Keys)
106
+ {
107
+ var seenIds = new List<string>();
108
+ string chain = null;
109
+ if (this.FindCircularReference(constraints, id, id, seenIds, out chain))
110
+ {
111
+ // We will show a separate message for every ID that's in
112
+ // the loop. We could bail after the first one, but then
113
+ // we wouldn't catch disjoint loops in a single run.
114
+ this.Messaging.Write(ErrorMessages.CircularSearchReference(chain));
115
+ }
116
+ }
117
+ }
118
+
119
+ /// <summary>
120
+ /// Recursive function that finds circular references in the constraints.
121
+ /// </summary>
122
+ /// <param name="constraints">Constraints to check.</param>
123
+ /// <param name="checkId">The identifier currently being looking for. (Fixed across a given run.)</param>
124
+ /// <param name="currentId">The idenifier curently being tested.</param>
125
+ /// <param name="seenIds">A list of identifiers seen, to ensure each identifier is only expanded once.</param>
126
+ /// <param name="chain">If a circular reference is found, will contain the chain of references.</param>
127
+ /// <returns>True if a circular reference is found, false otherwise.</returns>
128
+ private bool FindCircularReference(Constraints constraints, string checkId, string currentId, List<string> seenIds, out string chain)
129
+ {
130
+ chain = null;
131
+ if (constraints.TryGetValue(currentId, out var afterList))
132
+ {
133
+ foreach (string afterId in afterList)
134
+ {
135
+ if (afterId == checkId)
136
+ {
137
+ chain = String.Format(CultureInfo.InvariantCulture, "{0} -> {1}", currentId, afterId);
138
+ return true;
139
+ }
140
+
141
+ if (!seenIds.Contains(afterId))
142
+ {
143
+ seenIds.Add(afterId);
144
+ if (this.FindCircularReference(constraints, checkId, afterId, seenIds, out chain))
145
+ {
146
+ chain = String.Format(CultureInfo.InvariantCulture, "{0} -> {1}", currentId, chain);
147
+ return true;
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ return false;
154
+ }
155
+
156
+ /// <summary>
157
+ /// Flattens any dependency chains to simplify reordering.
158
+ /// </summary>
159
+ /// <param name="constraints"></param>
160
+ private void FlattenDependentReferences(Constraints constraints)
161
+ {
162
+ foreach (string id in constraints.Keys)
163
+ {
164
+ var flattenedIds = new List<string>();
165
+ this.AddDependentReferences(constraints, id, flattenedIds);
166
+ var constraintList = constraints[id];
167
+ foreach (var flattenedId in flattenedIds)
168
+ {
169
+ if (!constraintList.Contains(flattenedId))
170
+ {
171
+ constraintList.Add(flattenedId);
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ /// <summary>
178
+ /// Adds dependent references to a list.
179
+ /// </summary>
180
+ /// <param name="constraints"></param>
181
+ /// <param name="currentId"></param>
182
+ /// <param name="seenIds"></param>
183
+ private void AddDependentReferences(Constraints constraints, string currentId, List<string> seenIds)
184
+ {
185
+ if (constraints.TryGetValue(currentId, out var afterList))
186
+ {
187
+ foreach (var afterId in afterList)
188
+ {
189
+ if (!seenIds.Contains(afterId))
190
+ {
191
+ seenIds.Add(afterId);
192
+ this.AddDependentReferences(constraints, afterId, seenIds);
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ /// <summary>
199
+ /// Reorder by topological sort
200
+ /// </summary>
201
+ /// <remarks>
202
+ /// We use a variation of Kahn (1962) algorithm as described in
203
+ /// Wikipedia (http://en.wikipedia.org/wiki/Topological_sorting), with
204
+ /// the additional criteria that start nodes are sorted lexicographically
205
+ /// at each step to ensure a deterministic ordering based on 'after'
206
+ /// dependencies and ID.
207
+ /// </remarks>
208
+ private class TopologicalSort
209
+ {
210
+ private readonly List<string> startIds = new List<string>();
211
+ private Constraints constraints;
212
+
213
+ /// <summary>
214
+ /// Reorder by topological sort
215
+ /// </summary>
216
+ /// <param name="allIds">The complete list of IDs.</param>
217
+ /// <param name="constraints">Constraints to use.</param>
218
+ /// <returns>The topologically sorted list of IDs.</returns>
219
+ internal List<string> Sort(IEnumerable<string> allIds, Constraints constraints)
220
+ {
221
+ this.startIds.Clear();
222
+ this.CopyConstraints(constraints);
223
+
224
+ this.FindInitialStartIds(allIds);
225
+
226
+ // We always create a new sortedId list, because we return it
227
+ // to the caller and don't know what its lifetime may be.
228
+ var sortedIds = new List<string>();
229
+
230
+ while (this.startIds.Count > 0)
231
+ {
232
+ this.SortStartIds();
233
+
234
+ var currentId = this.startIds[0];
235
+ sortedIds.Add(currentId);
236
+ this.startIds.RemoveAt(0);
237
+
238
+ this.ResolveConstraint(currentId);
239
+ }
240
+
241
+ return sortedIds;
242
+ }
243
+
244
+ /// <summary>
245
+ /// Copies a Constraints set (to prevent modifying the incoming data).
246
+ /// </summary>
247
+ /// <param name="constraints">Constraints to copy.</param>
248
+ private void CopyConstraints(Constraints constraints)
249
+ {
250
+ this.constraints = new Constraints();
251
+ foreach (var id in constraints.Keys)
252
+ {
253
+ foreach (var afterId in constraints[id])
254
+ {
255
+ this.constraints.AddConstraint(id, afterId);
256
+ }
257
+ }
258
+ }
259
+
260
+ /// <summary>
261
+ /// Finds initial start IDs. (Those with no constraints.)
262
+ /// </summary>
263
+ /// <param name="allIds">The complete list of IDs.</param>
264
+ private void FindInitialStartIds(IEnumerable<string> allIds)
265
+ {
266
+ foreach (var id in allIds)
267
+ {
268
+ if (!this.constraints.ContainsKey(id))
269
+ {
270
+ this.startIds.Add(id);
271
+ }
272
+ }
273
+ }
274
+
275
+ /// <summary>
276
+ /// Sorts start IDs.
277
+ /// </summary>
278
+ private void SortStartIds()
279
+ {
280
+ this.startIds.Sort();
281
+ }
282
+
283
+ /// <summary>
284
+ /// Removes the resolved constraint and updates the list of startIds
285
+ /// with any now-valid (all constraints resolved) IDs.
286
+ /// </summary>
287
+ /// <param name="resolvedId">The ID to resolve from the set of constraints.</param>
288
+ private void ResolveConstraint(string resolvedId)
289
+ {
290
+ var newStartIds = new List<string>();
291
+
292
+ foreach (var id in this.constraints.Keys)
293
+ {
294
+ if (this.constraints[id].Contains(resolvedId))
295
+ {
296
+ this.constraints[id].Remove(resolvedId);
297
+
298
+ // If we just removed the last constraint for this
299
+ // ID, it is now a valid start ID.
300
+ if (this.constraints[id].Count == 0)
301
+ {
302
+ newStartIds.Add(id);
303
+ }
304
+ }
305
+ }
306
+
307
+ foreach (var id in newStartIds)
308
+ {
309
+ this.constraints.Remove(id);
310
+ }
311
+
312
+ this.startIds.AddRange(newStartIds);
313
+ }
314
+ }
315
+
316
+ private void OrderSearches(List<string> sortedIds, Dictionary<string, WixSearchTuple> searchTupleDictionary)
317
{
318
// TODO: Although the WixSearch tables are defined in the Util extension,
319
// the Bundle Binder has to know all about them. We hope to revisit all
@@ -42,22 +330,19 @@ namespace WixToolset.Core.Burn.Bundles
330
var extensionSearchesById = this.Section.Tuples
331
.Where(t => t.Definition.HasTag(BurnConstants.BundleExtensionSearchTupleDefinitionTag))
332
.ToDictionary(t => t.Id.Id);
45
- var searchTuples = this.Section.Tuples.OfType<WixSearchTuple>().ToList();
46
-
47
- this.ExtensionSearchTuplesByExtensionId = new Dictionary<string, IList<IntermediateTuple>>();
48
- this.OrderedSearchFacades = new List<ISearchFacade>(legacySearchesById.Keys.Count + setVariablesById.Keys.Count + extensionSearchesById.Keys.Count);
333
50
- foreach (var searchTuple in searchTuples)
334
+ foreach (var searchId in sortedIds)
335
{
52
- if (legacySearchesById.TryGetValue(searchTuple.Id.Id, out var specificSearchTuple))
336
+ var searchTuple = searchTupleDictionary[searchId];
337
+ if (legacySearchesById.TryGetValue(searchId, out var specificSearchTuple))
338
{
339
this.OrderedSearchFacades.Add(new LegacySearchFacade(searchTuple, specificSearchTuple));
340
}
56
- else if (setVariablesById.TryGetValue(searchTuple.Id.Id, out var setVariableTuple))
341
+ else if (setVariablesById.TryGetValue(searchId, out var setVariableTuple))
342
{
343
this.OrderedSearchFacades.Add(new SetVariableSearchFacade(searchTuple, setVariableTuple));
344
}
60
- else if (extensionSearchesById.TryGetValue(searchTuple.Id.Id, out var extensionSearchTuple))
345
+ else if (extensionSearchesById.TryGetValue(searchId, out var extensionSearchTuple))
346
{
347
this.OrderedSearchFacades.Add(new ExtensionSearchFacade(searchTuple));
348
@@ -70,7 +355,7 @@ namespace WixToolset.Core.Burn.Bundles
355
}
356
else
357
{
73
- this.Messaging.Write(ErrorMessages.MissingBundleSearch(searchTuple.SourceLineNumbers, searchTuple.Id.Id));
358
+ this.Messaging.Write(ErrorMessages.MissingBundleSearch(searchTuple.SourceLineNumbers, searchId));
359
}
360
}
361
}
src/test/Example.Extension/Data/example.wir
Binary files a/src/test/Example.Extension/Data/example.wir and /dev/null differ
src/test/WixToolsetTest.CoreIntegration/TestData/SetVariable/Simple.wxs
+1
-1
@@ -9,7 +9,7 @@
9
<SetVariable Id="SetCoercedString" Variable="CoercedString" Value="Bar" />
10
<SetVariable Id="SetCoercedVersion" Variable="CoercedVersion" Value="v2.0" />
11
<SetVariable Id="SetNeedsFormatting" Variable="NeedsFormatting" Value="[One] [Two] [Three]" />
12
+ <SetVariable Id="SetUnset" Variable="Unset" Condition="VersionString = v2.0" After="SetVersionString" />
13
<SetVariable Id="SetVersionString" Variable="VersionString" Value="v1.0" Type="string" />
13
- <SetVariable Id="SetUnset" Variable="Unset" Condition="VersionString = v2.0" />
14
</Fragment>
15
</Wix>