| Index: third_party/scons/scons-local/SCons/Taskmaster.py
|
| ===================================================================
|
| --- third_party/scons/scons-local/SCons/Taskmaster.py (revision 7505)
|
| +++ third_party/scons/scons-local/SCons/Taskmaster.py (working copy)
|
| @@ -48,7 +48,7 @@
|
| target(s) that it decides need to be evaluated and/or built.
|
| """
|
|
|
| -__revision__ = "src/engine/SCons/Taskmaster.py 3603 2008/10/10 05:46:45 scons"
|
| +__revision__ = "src/engine/SCons/Taskmaster.py 3842 2008/12/20 22:59:52 scons"
|
|
|
| from itertools import chain
|
| import operator
|
| @@ -138,6 +138,10 @@
|
| self.node = node
|
| self.exc_clear()
|
|
|
| + def trace_message(self, method, node, description='node'):
|
| + fmt = '%-20s %s %s\n'
|
| + return fmt % (method + ':', description, self.tm.trace_node(node))
|
| +
|
| def display(self, message):
|
| """
|
| Hook to allow the calling interface to display a message.
|
| @@ -159,6 +163,8 @@
|
| unlink underlying files and make all necessary directories before
|
| the Action is actually called to build the targets.
|
| """
|
| + T = self.tm.trace
|
| + if T: T.write(self.trace_message('Task.prepare()', self.node))
|
|
|
| # Now that it's the appropriate time, give the TaskMaster a
|
| # chance to raise any exceptions it encountered while preparing
|
| @@ -209,6 +215,8 @@
|
| so only do thread safe stuff here. Do thread unsafe stuff in
|
| prepare(), executed() or failed().
|
| """
|
| + T = self.tm.trace
|
| + if T: T.write(self.trace_message('Task.execute()', self.node))
|
|
|
| try:
|
| everything_was_cached = 1
|
| @@ -225,9 +233,11 @@
|
| raise
|
| except SCons.Errors.BuildError:
|
| raise
|
| - except:
|
| - raise SCons.Errors.TaskmasterException(self.targets[0],
|
| - sys.exc_info())
|
| + except Exception, e:
|
| + buildError = SCons.Errors.convert_to_BuildError(e)
|
| + buildError.node = self.targets[0]
|
| + buildError.exc_info = sys.exc_info()
|
| + raise buildError
|
|
|
| def executed_without_callbacks(self):
|
| """
|
| @@ -235,6 +245,10 @@
|
| and the Taskmaster instance doesn't want to call
|
| the Node's callback methods.
|
| """
|
| + T = self.tm.trace
|
| + if T: T.write(self.trace_message('Task.executed_without_callbacks()',
|
| + self.node))
|
| +
|
| for t in self.targets:
|
| if t.get_state() == NODE_EXECUTING:
|
| for side_effect in t.side_effects:
|
| @@ -254,6 +268,10 @@
|
| post-visit actions that must take place regardless of whether
|
| or not the target was an actual built target or a source Node.
|
| """
|
| + T = self.tm.trace
|
| + if T: T.write(self.trace_message('Task.executed_with_callbacks()',
|
| + self.node))
|
| +
|
| for t in self.targets:
|
| if t.get_state() == NODE_EXECUTING:
|
| for side_effect in t.side_effects:
|
| @@ -267,17 +285,30 @@
|
| def failed(self):
|
| """
|
| Default action when a task fails: stop the build.
|
| +
|
| + Note: Although this function is normally invoked on nodes in
|
| + the executing state, it might also be invoked on up-to-date
|
| + nodes when using Configure().
|
| """
|
| self.fail_stop()
|
|
|
| def fail_stop(self):
|
| """
|
| Explicit stop-the-build failure.
|
| +
|
| + This sets failure status on the target nodes and all of
|
| + their dependent parent nodes.
|
| +
|
| + Note: Although this function is normally invoked on nodes in
|
| + the executing state, it might also be invoked on up-to-date
|
| + nodes when using Configure().
|
| """
|
| -
|
| + T = self.tm.trace
|
| + if T: T.write(self.trace_message('Task.failed_stop()', self.node))
|
| +
|
| # Invoke will_not_build() to clean-up the pending children
|
| # list.
|
| - self.tm.will_not_build(self.targets)
|
| + self.tm.will_not_build(self.targets, lambda n: n.set_state(NODE_FAILED))
|
|
|
| # Tell the taskmaster to not start any new tasks
|
| self.tm.stop()
|
| @@ -294,9 +325,16 @@
|
|
|
| This sets failure status on the target nodes and all of
|
| their dependent parent nodes.
|
| +
|
| + Note: Although this function is normally invoked on nodes in
|
| + the executing state, it might also be invoked on up-to-date
|
| + nodes when using Configure().
|
| """
|
| - self.tm.will_not_build(self.targets)
|
| -
|
| + T = self.tm.trace
|
| + if T: T.write(self.trace_message('Task.failed_continue()', self.node))
|
| +
|
| + self.tm.will_not_build(self.targets, lambda n: n.set_state(NODE_FAILED))
|
| +
|
| def make_ready_all(self):
|
| """
|
| Marks all targets in a task ready for execution.
|
| @@ -304,6 +342,9 @@
|
| This is used when the interface needs every target Node to be
|
| visited--the canonical example being the "scons -c" option.
|
| """
|
| + T = self.tm.trace
|
| + if T: T.write(self.trace_message('Task.make_ready_all()', self.node))
|
| +
|
| self.out_of_date = self.targets[:]
|
| for t in self.targets:
|
| t.disambiguate().set_state(NODE_EXECUTING)
|
| @@ -317,6 +358,10 @@
|
|
|
| This is the default behavior for building only what's necessary.
|
| """
|
| + T = self.tm.trace
|
| + if T: T.write(self.trace_message('Task.make_ready_current()',
|
| + self.node))
|
| +
|
| self.out_of_date = []
|
| needs_executing = False
|
| for t in self.targets:
|
| @@ -336,7 +381,7 @@
|
| t.set_state(NODE_EXECUTING)
|
| for s in t.side_effects:
|
| s.set_state(NODE_EXECUTING)
|
| - else:
|
| + else:
|
| for t in self.targets:
|
| # We must invoke visited() to ensure that the node
|
| # information has been computed before allowing the
|
| @@ -357,6 +402,8 @@
|
| waiting parent Nodes, or Nodes waiting on a common side effect,
|
| that can be put back on the candidates list.
|
| """
|
| + T = self.tm.trace
|
| + if T: T.write(self.trace_message('Task.postprocess()', self.node))
|
|
|
| # We may have built multiple targets, some of which may have
|
| # common parents waiting for this build. Count up how many
|
| @@ -367,8 +414,16 @@
|
|
|
| targets = set(self.targets)
|
|
|
| + pending_children = self.tm.pending_children
|
| parents = {}
|
| for t in targets:
|
| + # A node can only be in the pending_children set if it has
|
| + # some waiting_parents.
|
| + if t.waiting_parents:
|
| + if T: T.write(self.trace_message('Task.postprocess()',
|
| + t,
|
| + 'removing'))
|
| + pending_children.discard(t)
|
| for p in t.waiting_parents:
|
| parents[p] = parents.get(p, 0) + 1
|
|
|
| @@ -381,13 +436,14 @@
|
| for p in s.waiting_s_e:
|
| if p.ref_count == 0:
|
| self.tm.candidates.append(p)
|
| - self.tm.pending_children.discard(p)
|
|
|
| for p, subtract in parents.items():
|
| p.ref_count = p.ref_count - subtract
|
| + if T: T.write(self.trace_message('Task.postprocess()',
|
| + p,
|
| + 'adjusted parent ref count'))
|
| if p.ref_count == 0:
|
| self.tm.candidates.append(p)
|
| - self.tm.pending_children.discard(p)
|
|
|
| for t in targets:
|
| t.postprocess()
|
| @@ -479,7 +535,6 @@
|
| self.next_candidate = self.find_next_candidate
|
| self.pending_children = set()
|
|
|
| -
|
| def find_next_candidate(self):
|
| """
|
| Returns the next candidate Node for (potential) evaluation.
|
| @@ -519,7 +574,7 @@
|
| def no_next_candidate(self):
|
| """
|
| Stops Taskmaster processing by not returning a next candidate.
|
| -
|
| +
|
| Note that we have to clean-up the Taskmaster candidate list
|
| because the cycle detection depends on the fact all nodes have
|
| been processed somehow.
|
| @@ -527,9 +582,96 @@
|
| while self.candidates:
|
| candidates = self.candidates
|
| self.candidates = []
|
| - self.will_not_build(candidates, lambda n: n.state < NODE_UP_TO_DATE)
|
| + self.will_not_build(candidates)
|
| return None
|
|
|
| + def _validate_pending_children(self):
|
| + """
|
| + Validate the content of the pending_children set. Assert if an
|
| + internal error is found.
|
| +
|
| + This function is used strictly for debugging the taskmaster by
|
| + checking that no invariants are violated. It is not used in
|
| + normal operation.
|
| +
|
| + The pending_children set is used to detect cycles in the
|
| + dependency graph. We call a "pending child" a child that is
|
| + found in the "pending" state when checking the dependencies of
|
| + its parent node.
|
| +
|
| + A pending child can occur when the Taskmaster completes a loop
|
| + through a cycle. For example, lets imagine a graph made of
|
| + three node (A, B and C) making a cycle. The evaluation starts
|
| + at node A. The taskmaster first consider whether node A's
|
| + child B is up-to-date. Then, recursively, node B needs to
|
| + check whether node C is up-to-date. This leaves us with a
|
| + dependency graph looking like:
|
| +
|
| + Next candidate \
|
| + \
|
| + Node A (Pending) --> Node B(Pending) --> Node C (NoState)
|
| + ^ |
|
| + | |
|
| + +-------------------------------------+
|
| +
|
| + Now, when the Taskmaster examines the Node C's child Node A,
|
| + it finds that Node A is in the "pending" state. Therefore,
|
| + Node A is a pending child of node C.
|
| +
|
| + Pending children indicate that the Taskmaster has potentially
|
| + loop back through a cycle. We say potentially because it could
|
| + also occur when a DAG is evaluated in parallel. For example,
|
| + consider the following graph:
|
| +
|
| +
|
| + Node A (Pending) --> Node B(Pending) --> Node C (Pending) --> ...
|
| + | ^
|
| + | |
|
| + +----------> Node D (NoState) --------+
|
| + /
|
| + Next candidate /
|
| +
|
| + The Taskmaster first evaluates the nodes A, B, and C and
|
| + starts building some children of node C. Assuming, that the
|
| + maximum parallel level has not been reached, the Taskmaster
|
| + will examine Node D. It will find that Node C is a pending
|
| + child of Node D.
|
| +
|
| + In summary, evaluating a graph with a cycle will always
|
| + involve a pending child at one point. A pending child might
|
| + indicate either a cycle or a diamond-shaped DAG. Only a
|
| + fraction of the nodes ends-up being a "pending child" of
|
| + another node. This keeps the pending_children set small in
|
| + practice.
|
| +
|
| + We can differentiate between the two cases if we wait until
|
| + the end of the build. At this point, all the pending children
|
| + nodes due to a diamond-shaped DAG will have been properly
|
| + built (or will have failed to build). But, the pending
|
| + children involved in a cycle will still be in the pending
|
| + state.
|
| +
|
| + The taskmaster removes nodes from the pending_children set as
|
| + soon as a pending_children node moves out of the pending
|
| + state. This also helps to keep the pending_children set small.
|
| + """
|
| +
|
| + for n in self.pending_children:
|
| + assert n.state in (NODE_PENDING, NODE_EXECUTING), \
|
| + (str(n), StateString[n.state])
|
| + assert len(n.waiting_parents) != 0, (str(n), len(n.waiting_parents))
|
| + for p in n.waiting_parents:
|
| + assert p.ref_count > 0, (str(n), str(p), p.ref_count)
|
| +
|
| +
|
| + def trace_message(self, message):
|
| + return 'Taskmaster: %s\n' % message
|
| +
|
| + def trace_node(self, node):
|
| + return '<%-10s %-3s %s>' % (StateString[node.get_state()],
|
| + node.ref_count,
|
| + repr(str(node)))
|
| +
|
| def _find_next_ready_node(self):
|
| """
|
| Finds the next node that is ready to be built.
|
| @@ -555,17 +697,25 @@
|
| self.ready_exc = None
|
|
|
| T = self.trace
|
| - if T: T.write('\nTaskmaster: Looking for a node to evaluate\n')
|
| + if T: T.write('\n' + self.trace_message('Looking for a node to evaluate'))
|
|
|
| while 1:
|
| node = self.next_candidate()
|
| if node is None:
|
| - if T: T.write('Taskmaster: No candidate anymore.\n\n')
|
| + if T: T.write(self.trace_message('No candidate anymore.') + '\n')
|
| return None
|
|
|
| node = node.disambiguate()
|
| state = node.get_state()
|
|
|
| + # For debugging only:
|
| + #
|
| + # try:
|
| + # self._validate_pending_children()
|
| + # except:
|
| + # self.ready_exc = sys.exc_info()
|
| + # return node
|
| +
|
| if CollectStats:
|
| if not hasattr(node, 'stats'):
|
| node.stats = Stats()
|
| @@ -575,8 +725,7 @@
|
| else:
|
| S = None
|
|
|
| - if T: T.write('Taskmaster: Considering node <%-10s %-3s %s> and its children:\n' %
|
| - (StateString[node.get_state()], node.ref_count, repr(str(node))))
|
| + if T: T.write(self.trace_message(' Considering node %s and its children:' % self.trace_node(node)))
|
|
|
| if state == NODE_NO_STATE:
|
| # Mark this node as being on the execution stack:
|
| @@ -584,7 +733,7 @@
|
| elif state > NODE_PENDING:
|
| # Skip this node if it has already been evaluated:
|
| if S: S.already_handled = S.already_handled + 1
|
| - if T: T.write('Taskmaster: already handled (executed)\n')
|
| + if T: T.write(self.trace_message(' already handled (executed)'))
|
| continue
|
|
|
| try:
|
| @@ -593,7 +742,7 @@
|
| exc_value = sys.exc_info()[1]
|
| e = SCons.Errors.ExplicitExit(node, exc_value.code)
|
| self.ready_exc = (SCons.Errors.ExplicitExit, e)
|
| - if T: T.write('Taskmaster: SystemExit\n')
|
| + if T: T.write(self.trace_message(' SystemExit'))
|
| return node
|
| except Exception, e:
|
| # We had a problem just trying to figure out the
|
| @@ -602,7 +751,7 @@
|
| # raise the exception when the Task is "executed."
|
| self.ready_exc = sys.exc_info()
|
| if S: S.problem = S.problem + 1
|
| - if T: T.write('Taskmaster: exception %s while scanning children.\n'%s)
|
| + if T: T.write(self.trace_message(' exception %s while scanning children.\n' % e))
|
| return node
|
|
|
| children_not_visited = []
|
| @@ -613,8 +762,7 @@
|
| for child in chain(children,node.prerequisites):
|
| childstate = child.get_state()
|
|
|
| - if T: T.write('Taskmaster: <%-10s %-3s %s>\n' %
|
| - (StateString[childstate], child.ref_count, repr(str(child))))
|
| + if T: T.write(self.trace_message(' ' + self.trace_node(child)))
|
|
|
| if childstate == NODE_NO_STATE:
|
| children_not_visited.append(child)
|
| @@ -633,8 +781,8 @@
|
| children_not_visited.reverse()
|
| self.candidates.extend(self.order(children_not_visited))
|
| #if T and children_not_visited:
|
| - # T.write('Taskmaster: adding to candidates: %s\n' % map(str, children_not_visited))
|
| - # T.write('Taskmaster: candidates now: %s\n' % map(str, self.candidates))
|
| + # T.write(self.trace_message(' adding to candidates: %s' % map(str, children_not_visited)))
|
| + # T.write(self.trace_message(' candidates now: %s\n' % map(str, self.candidates)))
|
|
|
| # Skip this node if any of its children have failed.
|
| #
|
| @@ -658,8 +806,7 @@
|
| node.set_state(NODE_FAILED)
|
|
|
| if S: S.child_failed = S.child_failed + 1
|
| - if T: T.write('Taskmaster:****** <%-10s %-3s %s>\n' %
|
| - (StateString[node.get_state()], node.ref_count, repr(str(node))))
|
| + if T: T.write(self.trace_message('****** %s\n' % self.trace_node(node)))
|
| continue
|
|
|
| if children_not_ready:
|
| @@ -673,11 +820,15 @@
|
| # count so we can be put back on the list for
|
| # re-evaluation when they've all finished.
|
| node.ref_count = node.ref_count + child.add_to_waiting_parents(node)
|
| - if T: T.write('Taskmaster: adjusting ref count: <%-10s %-3s %s>\n' %
|
| - (StateString[node.get_state()], node.ref_count, repr(str(node))))
|
| + if T: T.write(self.trace_message(' adjusted ref count: %s, child %s' %
|
| + (self.trace_node(node), repr(str(child)))))
|
|
|
| + if T:
|
| + for pc in children_pending:
|
| + T.write(self.trace_message(' adding %s to the pending children set\n' %
|
| + self.trace_node(pc)))
|
| self.pending_children = self.pending_children | children_pending
|
| -
|
| +
|
| continue
|
|
|
| # Skip this node if it has side-effects that are
|
| @@ -695,8 +846,17 @@
|
| # The default when we've gotten through all of the checks above:
|
| # this node is ready to be built.
|
| if S: S.build = S.build + 1
|
| - if T: T.write('Taskmaster: Evaluating <%-10s %-3s %s>\n' %
|
| - (StateString[node.get_state()], node.ref_count, repr(str(node))))
|
| + if T: T.write(self.trace_message('Evaluating %s\n' %
|
| + self.trace_node(node)))
|
| +
|
| + # For debugging only:
|
| + #
|
| + # try:
|
| + # self._validate_pending_children()
|
| + # except:
|
| + # self.ready_exc = sys.exc_info()
|
| + # return node
|
| +
|
| return node
|
|
|
| return None
|
| @@ -732,23 +892,24 @@
|
|
|
| return task
|
|
|
| - def will_not_build(self, nodes, mark_fail=lambda n: n.state != NODE_FAILED):
|
| + def will_not_build(self, nodes, node_func=lambda n: None):
|
| """
|
| - Perform clean-up about nodes that will never be built.
|
| + Perform clean-up about nodes that will never be built. Invokes
|
| + a user defined function on all of these nodes (including all
|
| + of their parents).
|
| """
|
|
|
| + T = self.trace
|
| +
|
| pending_children = self.pending_children
|
|
|
| - to_visit = set()
|
| - for node in nodes:
|
| - # Set failure state on all of the parents that were dependent
|
| - # on this failed build.
|
| - if mark_fail(node):
|
| - node.set_state(NODE_FAILED)
|
| - parents = node.waiting_parents
|
| - to_visit = to_visit | parents
|
| - pending_children = pending_children - parents
|
| + to_visit = set(nodes)
|
| + pending_children = pending_children - to_visit
|
|
|
| + if T:
|
| + for n in nodes:
|
| + T.write(self.trace_message(' removing node %s from the pending children set\n' %
|
| + self.trace_node(n)))
|
| try:
|
| while 1:
|
| try:
|
| @@ -760,11 +921,21 @@
|
| to_visit.remove(node)
|
| else:
|
| break
|
| - if mark_fail(node):
|
| - node.set_state(NODE_FAILED)
|
| - parents = node.waiting_parents
|
| - to_visit = to_visit | parents
|
| - pending_children = pending_children - parents
|
| +
|
| + node_func(node)
|
| +
|
| + # Prune recursion by flushing the waiting children
|
| + # list immediately.
|
| + parents = node.waiting_parents
|
| + node.waiting_parents = set()
|
| +
|
| + to_visit = to_visit | parents
|
| + pending_children = pending_children - parents
|
| +
|
| + for p in parents:
|
| + p.ref_count = p.ref_count - 1
|
| + if T: T.write(self.trace_message(' removing parent %s from the pending children set\n' %
|
| + self.trace_node(p)))
|
| except KeyError:
|
| # The container to_visit has been emptied.
|
| pass
|
| @@ -784,15 +955,31 @@
|
| """
|
| Check for dependency cycles.
|
| """
|
| - if self.pending_children:
|
| - desc = 'Found dependency cycle(s):\n'
|
| - for node in self.pending_children:
|
| - cycle = find_cycle([node], set())
|
| - if cycle:
|
| - desc = desc + " " + string.join(map(str, cycle), " -> ") + "\n"
|
| - else:
|
| - desc = desc + \
|
| - " Internal Error: no cycle found for node %s (%s) in state %s\n" % \
|
| - (node, repr(node), StateString[node.get_state()])
|
| + if not self.pending_children:
|
| + return
|
|
|
| - raise SCons.Errors.UserError, desc
|
| + # TODO(1.5)
|
| + #nclist = [ (n, find_cycle([n], set())) for n in self.pending_children ]
|
| + nclist = map(lambda n: (n, find_cycle([n], set())), self.pending_children)
|
| +
|
| + # TODO(1.5)
|
| + #genuine_cycles = [
|
| + # node for node, cycle in nclist
|
| + # if cycle or node.get_state() != NODE_EXECUTED
|
| + #]
|
| + genuine_cycles = filter(lambda t: t[1] or t[0].get_state() != NODE_EXECUTED, nclist)
|
| + if not genuine_cycles:
|
| + # All of the "cycles" found were single nodes in EXECUTED state,
|
| + # which is to say, they really weren't cycles. Just return.
|
| + return
|
| +
|
| + desc = 'Found dependency cycle(s):\n'
|
| + for node, cycle in nclist:
|
| + if cycle:
|
| + desc = desc + " " + string.join(map(str, cycle), " -> ") + "\n"
|
| + else:
|
| + desc = desc + \
|
| + " Internal Error: no cycle found for node %s (%s) in state %s\n" % \
|
| + (node, repr(node), StateString[node.get_state()])
|
| +
|
| + raise SCons.Errors.UserError, desc
|
|
|