|
| 1 | +# swift_build_support/build_graph.py ----------------------------*- python -*- |
| 2 | +# |
| 3 | +# This source file is part of the Swift.org open source project |
| 4 | +# |
| 5 | +# Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors |
| 6 | +# Licensed under Apache License v2.0 with Runtime Library Exception |
| 7 | +# |
| 8 | +# See https://swift.org/LICENSE.txt for license information |
| 9 | +# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors |
| 10 | +# |
| 11 | +# ---------------------------------------------------------------------------- |
| 12 | +# |
| 13 | +# This is a simple implementation of an acyclic build graph. We require no |
| 14 | +# cycles, so we just perform a reverse post order traversal to get a topological |
| 15 | +# ordering. We check during the reverse post order traversal that we do not |
| 16 | +# visit any node multiple times. |
| 17 | +# |
| 18 | +# Nodes are assumed to be a product's class. |
| 19 | +# |
| 20 | +# ---------------------------------------------------------------------------- |
| 21 | + |
| 22 | + |
| 23 | +def _get_po_ordered_nodes(root, invertedDepMap): |
| 24 | + # Then setup our worklist/visited node set. |
| 25 | + worklist = [root] |
| 26 | + visitedNodes = set([]) |
| 27 | + # TODO: Can we unify po_ordered_nodes and visitedNodes in some way? |
| 28 | + po_ordered_nodes = [] |
| 29 | + |
| 30 | + # Until we no longer have nodes to visit... |
| 31 | + while not len(worklist) == 0: |
| 32 | + # First grab the last element of the worklist. If we have already |
| 33 | + # visited this node, just pop it and skip it. |
| 34 | + # |
| 35 | + # DISCUSSION: Consider the following build graph: |
| 36 | + # |
| 37 | + # A -> [C, B] |
| 38 | + # B -> [C] |
| 39 | + # |
| 40 | + # In this case, we will most likely get the following worklist |
| 41 | + # before actually processing anything: |
| 42 | + # |
| 43 | + # A, C, B, C |
| 44 | + # |
| 45 | + # In this case, we want to ignore the initial C pushed onto the |
| 46 | + # worklist by visiting A since we will have visited C already due to |
| 47 | + # the edge from B -> C. |
| 48 | + node = worklist[-1] |
| 49 | + if node in visitedNodes: |
| 50 | + worklist.pop() |
| 51 | + continue |
| 52 | + |
| 53 | + # Then grab the dependents of our node. |
| 54 | + deps = invertedDepMap.get(node, set([])) |
| 55 | + assert(isinstance(deps, set)) |
| 56 | + |
| 57 | + # Then visit those and see if we have not visited any of them. Push |
| 58 | + # any such nodes onto the worklist and continue. If we have already |
| 59 | + # visited all of our dependents, then we can actually process this |
| 60 | + # node. |
| 61 | + foundDep = False |
| 62 | + for d in deps: |
| 63 | + if d not in visitedNodes: |
| 64 | + foundDep = True |
| 65 | + worklist.append(d) |
| 66 | + if foundDep: |
| 67 | + continue |
| 68 | + |
| 69 | + # Now process the node by popping it off the worklist, adding it to |
| 70 | + # the visited nodes set, and append it to the po_ordered_nodes in |
| 71 | + # its final position. |
| 72 | + worklist.pop() |
| 73 | + visitedNodes.add(node) |
| 74 | + po_ordered_nodes.append(node) |
| 75 | + return po_ordered_nodes |
| 76 | + |
| 77 | + |
| 78 | +class BuildDAG(object): |
| 79 | + |
| 80 | + def __init__(self): |
| 81 | + self.root = None |
| 82 | + |
| 83 | + # A map from a node to a list of nodes that depend on the given node. |
| 84 | + # |
| 85 | + # NOTE: This is an inverted dependency map implying that the root will |
| 86 | + # be a "final element" of the graph. |
| 87 | + self.invertedDepMap = {} |
| 88 | + |
| 89 | + def add_edge(self, pred, succ): |
| 90 | + self.invertedDepMap.setdefault(pred, set([succ])) \ |
| 91 | + .add(succ) |
| 92 | + |
| 93 | + def set_root(self, root): |
| 94 | + # Assert that we always only have one root. |
| 95 | + assert(self.root is None) |
| 96 | + self.root = root |
| 97 | + |
| 98 | + def produce_schedule(self): |
| 99 | + # Grab the root and make sure it is not None |
| 100 | + root = self.root |
| 101 | + assert(root is not None) |
| 102 | + |
| 103 | + # Then perform a post order traversal from root using our inverted |
| 104 | + # dependency map to compute a list of our nodes in post order. |
| 105 | + # |
| 106 | + # NOTE: The index of each node in this list is the post order number of |
| 107 | + # the node. |
| 108 | + po_ordered_nodes = _get_po_ordered_nodes(root, self.invertedDepMap) |
| 109 | + |
| 110 | + # Ok, we have our post order list. We want to provide our user a reverse |
| 111 | + # post order, so we take our array and construct a dictionary of an |
| 112 | + # enumeration of the list. This will give us a dictionary mapping our |
| 113 | + # product names to their reverse post order number. |
| 114 | + rpo_ordered_nodes = list(reversed(po_ordered_nodes)) |
| 115 | + node_to_rpot_map = dict((y, x) for x, y in enumerate(rpo_ordered_nodes)) |
| 116 | + |
| 117 | + # Now before we return our rpo_ordered_nodes and our node_to_rpot_map, lets |
| 118 | + # verify that we didn't find any cycles. We can do this by traversing |
| 119 | + # our dependency graph in reverse post order and making sure all |
| 120 | + # dependencies of each node we visit has a later reverse post order |
| 121 | + # number than the node we are checking. |
| 122 | + for n, node in enumerate(rpo_ordered_nodes): |
| 123 | + for dep in self.invertedDepMap.get(node, []): |
| 124 | + if node_to_rpot_map[dep] < n: |
| 125 | + print('n: {}. node: {}.'.format(n, node)) |
| 126 | + print('dep: {}.'.format(dep)) |
| 127 | + print('inverted dependency map: {}'.format(self.invertedDepMap)) |
| 128 | + print('rpo ordered nodes: {}'.format(rpo_ordered_nodes)) |
| 129 | + print('rpo node to rpo number map: {}'.format(node_to_rpot_map)) |
| 130 | + raise RuntimeError('Found cycle in build graph!') |
| 131 | + |
| 132 | + return (rpo_ordered_nodes, node_to_rpot_map) |
| 133 | + |
| 134 | + |
| 135 | +def produce_scheduled_build(input_product_classes): |
| 136 | + """For a given a subset input_input_product_classes of |
| 137 | + all_input_product_classes, compute a topological ordering of the |
| 138 | + input_input_product_classes + topological closures that respects the |
| 139 | + dependency graph. |
| 140 | + """ |
| 141 | + dag = BuildDAG() |
| 142 | + worklist = list(input_product_classes) |
| 143 | + visited = set(input_product_classes) |
| 144 | + |
| 145 | + # Construct the DAG. |
| 146 | + while len(worklist) > 0: |
| 147 | + entry = worklist.pop() |
| 148 | + deps = entry.get_dependencies() |
| 149 | + if len(deps) == 0: |
| 150 | + dag.set_root(entry) |
| 151 | + for d in deps: |
| 152 | + dag.add_edge(d, entry) |
| 153 | + if d not in visited: |
| 154 | + worklist.append(d) |
| 155 | + visited = visited.union(deps) |
| 156 | + |
| 157 | + # Then produce the schedule. |
| 158 | + schedule = dag.produce_schedule() |
| 159 | + |
| 160 | + # Finally check that all of our input_product_classes are in the schedule. |
| 161 | + if len(set(input_product_classes) - set(schedule[0])) != 0: |
| 162 | + raise RuntimeError('Found disconnected graph?!') |
| 163 | + |
| 164 | + return schedule |
0 commit comments