@joebigelow / wix-1 / commits / bf435c69

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>