1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 __doc__ = """
24 Generic Taskmaster module for the SCons build engine.
25
26 This module contains the primary interface(s) between a wrapping user
27 interface and the SCons build engine. There are two key classes here:
28
29 Taskmaster
30 This is the main engine for walking the dependency graph and
31 calling things to decide what does or doesn't need to be built.
32
33 Task
34 This is the base class for allowing a wrapping interface to
35 decide what does or doesn't actually need to be done. The
36 intention is for a wrapping interface to subclass this as
37 appropriate for different types of behavior it may need.
38
39 The canonical example is the SCons native Python interface,
40 which has Task subclasses that handle its specific behavior,
41 like printing "`foo' is up to date" when a top-level target
42 doesn't need to be built, and handling the -c option by removing
43 targets as its "build" action. There is also a separate subclass
44 for suppressing this output when the -q option is used.
45
46 The Taskmaster instantiates a Task object for each (set of)
47 target(s) that it decides need to be evaluated and/or built.
48 """
49
50 __revision__ = "src/engine/SCons/Taskmaster.py 2013/03/03 09:48:35 garyo"
51
52 from itertools import chain
53 import operator
54 import sys
55 import traceback
56
57 import SCons.Errors
58 import SCons.Node
59 import SCons.Warnings
60
61 StateString = SCons.Node.StateString
62 NODE_NO_STATE = SCons.Node.no_state
63 NODE_PENDING = SCons.Node.pending
64 NODE_EXECUTING = SCons.Node.executing
65 NODE_UP_TO_DATE = SCons.Node.up_to_date
66 NODE_EXECUTED = SCons.Node.executed
67 NODE_FAILED = SCons.Node.failed
68
69 print_prepare = 0
70
71
72
73
74
75 CollectStats = None
76
78 """
79 A simple class for holding statistics about the disposition of a
80 Node by the Taskmaster. If we're collecting statistics, each Node
81 processed by the Taskmaster gets one of these attached, in which case
82 the Taskmaster records its decision each time it processes the Node.
83 (Ideally, that's just once per Node.)
84 """
86 """
87 Instantiates a Taskmaster.Stats object, initializing all
88 appropriate counters to zero.
89 """
90 self.considered = 0
91 self.already_handled = 0
92 self.problem = 0
93 self.child_failed = 0
94 self.not_built = 0
95 self.side_effects = 0
96 self.build = 0
97
98 StatsNodes = []
99
100 fmt = "%(considered)3d "\
101 "%(already_handled)3d " \
102 "%(problem)3d " \
103 "%(child_failed)3d " \
104 "%(not_built)3d " \
105 "%(side_effects)3d " \
106 "%(build)3d "
107
109 for n in sorted(StatsNodes, key=lambda a: str(a)):
110 print (fmt % n.stats.__dict__) + str(n)
111
112
113
115 """
116 Default SCons build engine task.
117
118 This controls the interaction of the actual building of node
119 and the rest of the engine.
120
121 This is expected to handle all of the normally-customizable
122 aspects of controlling a build, so any given application
123 *should* be able to do what it wants by sub-classing this
124 class and overriding methods as appropriate. If an application
125 needs to customze something by sub-classing Taskmaster (or
126 some other build engine class), we should first try to migrate
127 that functionality into this class.
128
129 Note that it's generally a good idea for sub-classes to call
130 these methods explicitly to update state, etc., rather than
131 roll their own interaction with Taskmaster from scratch.
132 """
133 - def __init__(self, tm, targets, top, node):
134 self.tm = tm
135 self.targets = targets
136 self.top = top
137 self.node = node
138 self.exc_clear()
139
141 fmt = '%-20s %s %s\n'
142 return fmt % (method + ':', description, self.tm.trace_node(node))
143
145 """
146 Hook to allow the calling interface to display a message.
147
148 This hook gets called as part of preparing a task for execution
149 (that is, a Node to be built). As part of figuring out what Node
150 should be built next, the actually target list may be altered,
151 along with a message describing the alteration. The calling
152 interface can subclass Task and provide a concrete implementation
153 of this method to see those messages.
154 """
155 pass
156
158 """
159 Called just before the task is executed.
160
161 This is mainly intended to give the target Nodes a chance to
162 unlink underlying files and make all necessary directories before
163 the Action is actually called to build the targets.
164 """
165 global print_prepare
166 T = self.tm.trace
167 if T: T.write(self.trace_message(u'Task.prepare()', self.node))
168
169
170
171
172 self.exception_raise()
173
174 if self.tm.message:
175 self.display(self.tm.message)
176 self.tm.message = None
177
178
179
180
181
182
183
184
185
186
187
188 executor = self.targets[0].get_executor()
189 executor.prepare()
190 for t in executor.get_action_targets():
191 if print_prepare:
192 print "Preparing target %s..."%t
193 for s in t.side_effects:
194 print "...with side-effect %s..."%s
195 t.prepare()
196 for s in t.side_effects:
197 if print_prepare:
198 print "...Preparing side-effect %s..."%s
199 s.prepare()
200
202 """Fetch the target being built or updated by this task.
203 """
204 return self.node
205
217
219 """
220 Called to execute the task.
221
222 This method is called from multiple threads in a parallel build,
223 so only do thread safe stuff here. Do thread unsafe stuff in
224 prepare(), executed() or failed().
225 """
226 T = self.tm.trace
227 if T: T.write(self.trace_message(u'Task.execute()', self.node))
228
229 try:
230 cached_targets = []
231 for t in self.targets:
232 if not t.retrieve_from_cache():
233 break
234 cached_targets.append(t)
235 if len(cached_targets) < len(self.targets):
236
237
238
239
240
241 for t in cached_targets:
242 try:
243 t.fs.unlink(t.path)
244 except (IOError, OSError):
245 pass
246 self.targets[0].build()
247 else:
248 for t in cached_targets:
249 t.cached = 1
250 except SystemExit:
251 exc_value = sys.exc_info()[1]
252 raise SCons.Errors.ExplicitExit(self.targets[0], exc_value.code)
253 except SCons.Errors.UserError:
254 raise
255 except SCons.Errors.BuildError:
256 raise
257 except Exception, e:
258 buildError = SCons.Errors.convert_to_BuildError(e)
259 buildError.node = self.targets[0]
260 buildError.exc_info = sys.exc_info()
261 raise buildError
262
264 """
265 Called when the task has been successfully executed
266 and the Taskmaster instance doesn't want to call
267 the Node's callback methods.
268 """
269 T = self.tm.trace
270 if T: T.write(self.trace_message('Task.executed_without_callbacks()',
271 self.node))
272
273 for t in self.targets:
274 if t.get_state() == NODE_EXECUTING:
275 for side_effect in t.side_effects:
276 side_effect.set_state(NODE_NO_STATE)
277 t.set_state(NODE_EXECUTED)
278
280 """
281 Called when the task has been successfully executed and
282 the Taskmaster instance wants to call the Node's callback
283 methods.
284
285 This may have been a do-nothing operation (to preserve build
286 order), so we must check the node's state before deciding whether
287 it was "built", in which case we call the appropriate Node method.
288 In any event, we always call "visited()", which will handle any
289 post-visit actions that must take place regardless of whether
290 or not the target was an actual built target or a source Node.
291 """
292 T = self.tm.trace
293 if T: T.write(self.trace_message('Task.executed_with_callbacks()',
294 self.node))
295
296 for t in self.targets:
297 if t.get_state() == NODE_EXECUTING:
298 for side_effect in t.side_effects:
299 side_effect.set_state(NODE_NO_STATE)
300 t.set_state(NODE_EXECUTED)
301 if not t.cached:
302 t.push_to_cache()
303 t.built()
304 t.visited()
305
306 executed = executed_with_callbacks
307
309 """
310 Default action when a task fails: stop the build.
311
312 Note: Although this function is normally invoked on nodes in
313 the executing state, it might also be invoked on up-to-date
314 nodes when using Configure().
315 """
316 self.fail_stop()
317
319 """
320 Explicit stop-the-build failure.
321
322 This sets failure status on the target nodes and all of
323 their dependent parent nodes.
324
325 Note: Although this function is normally invoked on nodes in
326 the executing state, it might also be invoked on up-to-date
327 nodes when using Configure().
328 """
329 T = self.tm.trace
330 if T: T.write(self.trace_message('Task.failed_stop()', self.node))
331
332
333
334 self.tm.will_not_build(self.targets, lambda n: n.set_state(NODE_FAILED))
335
336
337 self.tm.stop()
338
339
340
341
342 self.targets = [self.tm.current_top]
343 self.top = 1
344
346 """
347 Explicit continue-the-build failure.
348
349 This sets failure status on the target nodes and all of
350 their dependent parent nodes.
351
352 Note: Although this function is normally invoked on nodes in
353 the executing state, it might also be invoked on up-to-date
354 nodes when using Configure().
355 """
356 T = self.tm.trace
357 if T: T.write(self.trace_message('Task.failed_continue()', self.node))
358
359 self.tm.will_not_build(self.targets, lambda n: n.set_state(NODE_FAILED))
360
362 """
363 Marks all targets in a task ready for execution.
364
365 This is used when the interface needs every target Node to be
366 visited--the canonical example being the "scons -c" option.
367 """
368 T = self.tm.trace
369 if T: T.write(self.trace_message('Task.make_ready_all()', self.node))
370
371 self.out_of_date = self.targets[:]
372 for t in self.targets:
373 t.disambiguate().set_state(NODE_EXECUTING)
374 for s in t.side_effects:
375
376 s.disambiguate().set_state(NODE_EXECUTING)
377
417
418 make_ready = make_ready_current
419
420 - def postprocess(self):
421 """
422 Post-processes a task after it's been executed.
423
424 This examines all the targets just built (or not, we don't care
425 if the build was successful, or even if there was no build
426 because everything was up-to-date) to see if they have any
427 waiting parent Nodes, or Nodes waiting on a common side effect,
428 that can be put back on the candidates list.
429 """
430 T = self.tm.trace
431 if T: T.write(self.trace_message(u'Task.postprocess()', self.node))
432
433
434
435
436
437
438
439
440 targets = set(self.targets)
441
442 pending_children = self.tm.pending_children
443 parents = {}
444 for t in targets:
445
446
447 if t.waiting_parents:
448 if T: T.write(self.trace_message(u'Task.postprocess()',
449 t,
450 'removing'))
451 pending_children.discard(t)
452 for p in t.waiting_parents:
453 parents[p] = parents.get(p, 0) + 1
454
455 for t in targets:
456 for s in t.side_effects:
457 if s.get_state() == NODE_EXECUTING:
458 s.set_state(NODE_NO_STATE)
459 for p in s.waiting_parents:
460 parents[p] = parents.get(p, 0) + 1
461 for p in s.waiting_s_e:
462 if p.ref_count == 0:
463 self.tm.candidates.append(p)
464
465 for p, subtract in parents.items():
466 p.ref_count = p.ref_count - subtract
467 if T: T.write(self.trace_message(u'Task.postprocess()',
468 p,
469 'adjusted parent ref count'))
470 if p.ref_count == 0:
471 self.tm.candidates.append(p)
472
473 for t in targets:
474 t.postprocess()
475
476
477
478
479
480
481
482
483
484
486 """
487 Returns info about a recorded exception.
488 """
489 return self.exception
490
492 """
493 Clears any recorded exception.
494
495 This also changes the "exception_raise" attribute to point
496 to the appropriate do-nothing method.
497 """
498 self.exception = (None, None, None)
499 self.exception_raise = self._no_exception_to_raise
500
502 """
503 Records an exception to be raised at the appropriate time.
504
505 This also changes the "exception_raise" attribute to point
506 to the method that will, in fact
507 """
508 if not exception:
509 exception = sys.exc_info()
510 self.exception = exception
511 self.exception_raise = self._exception_raise
512
515
517 """
518 Raises a pending exception that was recorded while getting a
519 Task ready for execution.
520 """
521 exc = self.exc_info()[:]
522 try:
523 exc_type, exc_value, exc_traceback = exc
524 except ValueError:
525 exc_type, exc_value = exc
526 exc_traceback = None
527 raise exc_type, exc_value, exc_traceback
528
531 """
532 Always returns True (indicating this Task should always
533 be executed).
534
535 Subclasses that need this behavior (as opposed to the default
536 of only executing Nodes that are out of date w.r.t. their
537 dependencies) can use this as follows:
538
539 class MyTaskSubclass(SCons.Taskmaster.Task):
540 needs_execute = SCons.Taskmaster.Task.execute_always
541 """
542 return True
543
546 """
547 Returns True (indicating this Task should be executed) if this
548 Task's target state indicates it needs executing, which has
549 already been determined by an earlier up-to-date check.
550 """
551 return self.targets[0].get_state() == SCons.Node.executing
552
553
555 if stack[-1] in visited:
556 return None
557 visited.add(stack[-1])
558 for n in stack[-1].waiting_parents:
559 stack.append(n)
560 if stack[0] == stack[-1]:
561 return stack
562 if find_cycle(stack, visited):
563 return stack
564 stack.pop()
565 return None
566
567
569 """
570 The Taskmaster for walking the dependency DAG.
571 """
572
573 - def __init__(self, targets=[], tasker=None, order=None, trace=None):
574 self.original_top = targets
575 self.top_targets_left = targets[:]
576 self.top_targets_left.reverse()
577 self.candidates = []
578 if tasker is None:
579 tasker = OutOfDateTask
580 self.tasker = tasker
581 if not order:
582 order = lambda l: l
583 self.order = order
584 self.message = None
585 self.trace = trace
586 self.next_candidate = self.find_next_candidate
587 self.pending_children = set()
588
590 """
591 Returns the next candidate Node for (potential) evaluation.
592
593 The candidate list (really a stack) initially consists of all of
594 the top-level (command line) targets provided when the Taskmaster
595 was initialized. While we walk the DAG, visiting Nodes, all the
596 children that haven't finished processing get pushed on to the
597 candidate list. Each child can then be popped and examined in
598 turn for whether *their* children are all up-to-date, in which
599 case a Task will be created for their actual evaluation and
600 potential building.
601
602 Here is where we also allow candidate Nodes to alter the list of
603 Nodes that should be examined. This is used, for example, when
604 invoking SCons in a source directory. A source directory Node can
605 return its corresponding build directory Node, essentially saying,
606 "Hey, you really need to build this thing over here instead."
607 """
608 try:
609 return self.candidates.pop()
610 except IndexError:
611 pass
612 try:
613 node = self.top_targets_left.pop()
614 except IndexError:
615 return None
616 self.current_top = node
617 alt, message = node.alter_targets()
618 if alt:
619 self.message = message
620 self.candidates.append(node)
621 self.candidates.extend(self.order(alt))
622 node = self.candidates.pop()
623 return node
624
626 """
627 Stops Taskmaster processing by not returning a next candidate.
628
629 Note that we have to clean-up the Taskmaster candidate list
630 because the cycle detection depends on the fact all nodes have
631 been processed somehow.
632 """
633 while self.candidates:
634 candidates = self.candidates
635 self.candidates = []
636 self.will_not_build(candidates)
637 return None
638
640 """
641 Validate the content of the pending_children set. Assert if an
642 internal error is found.
643
644 This function is used strictly for debugging the taskmaster by
645 checking that no invariants are violated. It is not used in
646 normal operation.
647
648 The pending_children set is used to detect cycles in the
649 dependency graph. We call a "pending child" a child that is
650 found in the "pending" state when checking the dependencies of
651 its parent node.
652
653 A pending child can occur when the Taskmaster completes a loop
654 through a cycle. For example, lets imagine a graph made of
655 three node (A, B and C) making a cycle. The evaluation starts
656 at node A. The taskmaster first consider whether node A's
657 child B is up-to-date. Then, recursively, node B needs to
658 check whether node C is up-to-date. This leaves us with a
659 dependency graph looking like:
660
661 Next candidate \
662 \
663 Node A (Pending) --> Node B(Pending) --> Node C (NoState)
664 ^ |
665 | |
666 +-------------------------------------+
667
668 Now, when the Taskmaster examines the Node C's child Node A,
669 it finds that Node A is in the "pending" state. Therefore,
670 Node A is a pending child of node C.
671
672 Pending children indicate that the Taskmaster has potentially
673 loop back through a cycle. We say potentially because it could
674 also occur when a DAG is evaluated in parallel. For example,
675 consider the following graph:
676
677
678 Node A (Pending) --> Node B(Pending) --> Node C (Pending) --> ...
679 | ^
680 | |
681 +----------> Node D (NoState) --------+
682 /
683 Next candidate /
684
685 The Taskmaster first evaluates the nodes A, B, and C and
686 starts building some children of node C. Assuming, that the
687 maximum parallel level has not been reached, the Taskmaster
688 will examine Node D. It will find that Node C is a pending
689 child of Node D.
690
691 In summary, evaluating a graph with a cycle will always
692 involve a pending child at one point. A pending child might
693 indicate either a cycle or a diamond-shaped DAG. Only a
694 fraction of the nodes ends-up being a "pending child" of
695 another node. This keeps the pending_children set small in
696 practice.
697
698 We can differentiate between the two cases if we wait until
699 the end of the build. At this point, all the pending children
700 nodes due to a diamond-shaped DAG will have been properly
701 built (or will have failed to build). But, the pending
702 children involved in a cycle will still be in the pending
703 state.
704
705 The taskmaster removes nodes from the pending_children set as
706 soon as a pending_children node moves out of the pending
707 state. This also helps to keep the pending_children set small.
708 """
709
710 for n in self.pending_children:
711 assert n.state in (NODE_PENDING, NODE_EXECUTING), \
712 (str(n), StateString[n.state])
713 assert len(n.waiting_parents) != 0, (str(n), len(n.waiting_parents))
714 for p in n.waiting_parents:
715 assert p.ref_count > 0, (str(n), str(p), p.ref_count)
716
717
719 return 'Taskmaster: %s\n' % message
720
722 return '<%-10s %-3s %s>' % (StateString[node.get_state()],
723 node.ref_count,
724 repr(str(node)))
725
727 """
728 Finds the next node that is ready to be built.
729
730 This is *the* main guts of the DAG walk. We loop through the
731 list of candidates, looking for something that has no un-built
732 children (i.e., that is a leaf Node or has dependencies that are
733 all leaf Nodes or up-to-date). Candidate Nodes are re-scanned
734 (both the target Node itself and its sources, which are always
735 scanned in the context of a given target) to discover implicit
736 dependencies. A Node that must wait for some children to be
737 built will be put back on the candidates list after the children
738 have finished building. A Node that has been put back on the
739 candidates list in this way may have itself (or its sources)
740 re-scanned, in order to handle generated header files (e.g.) and
741 the implicit dependencies therein.
742
743 Note that this method does not do any signature calculation or
744 up-to-date check itself. All of that is handled by the Task
745 class. This is purely concerned with the dependency graph walk.
746 """
747
748 self.ready_exc = None
749
750 T = self.trace
751 if T: T.write(u'\n' + self.trace_message('Looking for a node to evaluate'))
752
753 while True:
754 node = self.next_candidate()
755 if node is None:
756 if T: T.write(self.trace_message('No candidate anymore.') + u'\n')
757 return None
758
759 node = node.disambiguate()
760 state = node.get_state()
761
762
763
764
765
766
767
768
769
770 if CollectStats:
771 if not hasattr(node, 'stats'):
772 node.stats = Stats()
773 StatsNodes.append(node)
774 S = node.stats
775 S.considered = S.considered + 1
776 else:
777 S = None
778
779 if T: T.write(self.trace_message(u' Considering node %s and its children:' % self.trace_node(node)))
780
781 if state == NODE_NO_STATE:
782
783 node.set_state(NODE_PENDING)
784 elif state > NODE_PENDING:
785
786 if S: S.already_handled = S.already_handled + 1
787 if T: T.write(self.trace_message(u' already handled (executed)'))
788 continue
789
790 executor = node.get_executor()
791
792 try:
793 children = executor.get_all_children()
794 except SystemExit:
795 exc_value = sys.exc_info()[1]
796 e = SCons.Errors.ExplicitExit(node, exc_value.code)
797 self.ready_exc = (SCons.Errors.ExplicitExit, e)
798 if T: T.write(self.trace_message(' SystemExit'))
799 return node
800 except Exception, e:
801
802
803
804
805 self.ready_exc = sys.exc_info()
806 if S: S.problem = S.problem + 1
807 if T: T.write(self.trace_message(' exception %s while scanning children.\n' % e))
808 return node
809
810 children_not_visited = []
811 children_pending = set()
812 children_not_ready = []
813 children_failed = False
814
815 for child in chain(executor.get_all_prerequisites(), children):
816 childstate = child.get_state()
817
818 if T: T.write(self.trace_message(u' ' + self.trace_node(child)))
819
820 if childstate == NODE_NO_STATE:
821 children_not_visited.append(child)
822 elif childstate == NODE_PENDING:
823 children_pending.add(child)
824 elif childstate == NODE_FAILED:
825 children_failed = True
826
827 if childstate <= NODE_EXECUTING:
828 children_not_ready.append(child)
829
830
831
832
833
834 children_not_visited.reverse()
835 self.candidates.extend(self.order(children_not_visited))
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858 if children_failed:
859 for n in executor.get_action_targets():
860 n.set_state(NODE_FAILED)
861
862 if S: S.child_failed = S.child_failed + 1
863 if T: T.write(self.trace_message('****** %s\n' % self.trace_node(node)))
864 continue
865
866 if children_not_ready:
867 for child in children_not_ready:
868
869
870 if S: S.not_built = S.not_built + 1
871
872
873
874
875
876 node.ref_count = node.ref_count + child.add_to_waiting_parents(node)
877 if T: T.write(self.trace_message(u' adjusted ref count: %s, child %s' %
878 (self.trace_node(node), repr(str(child)))))
879
880 if T:
881 for pc in children_pending:
882 T.write(self.trace_message(' adding %s to the pending children set\n' %
883 self.trace_node(pc)))
884 self.pending_children = self.pending_children | children_pending
885
886 continue
887
888
889
890 wait_side_effects = False
891 for se in executor.get_action_side_effects():
892 if se.get_state() == NODE_EXECUTING:
893 se.add_to_waiting_s_e(node)
894 wait_side_effects = True
895
896 if wait_side_effects:
897 if S: S.side_effects = S.side_effects + 1
898 continue
899
900
901
902 if S: S.build = S.build + 1
903 if T: T.write(self.trace_message(u'Evaluating %s\n' %
904 self.trace_node(node)))
905
906
907
908
909
910
911
912
913
914 return node
915
916 return None
917
919 """
920 Returns the next task to be executed.
921
922 This simply asks for the next Node to be evaluated, and then wraps
923 it in the specific Task subclass with which we were initialized.
924 """
925 node = self._find_next_ready_node()
926
927 if node is None:
928 return None
929
930 tlist = node.get_executor().get_all_targets()
931
932 task = self.tasker(self, tlist, node in self.original_top, node)
933 try:
934 task.make_ready()
935 except:
936
937
938
939
940 self.ready_exc = sys.exc_info()
941
942 if self.ready_exc:
943 task.exception_set(self.ready_exc)
944
945 self.ready_exc = None
946
947 return task
948
950 """
951 Perform clean-up about nodes that will never be built. Invokes
952 a user defined function on all of these nodes (including all
953 of their parents).
954 """
955
956 T = self.trace
957
958 pending_children = self.pending_children
959
960 to_visit = set(nodes)
961 pending_children = pending_children - to_visit
962
963 if T:
964 for n in nodes:
965 T.write(self.trace_message(' removing node %s from the pending children set\n' %
966 self.trace_node(n)))
967 try:
968 while len(to_visit):
969 node = to_visit.pop()
970 node_func(node)
971
972
973
974 parents = node.waiting_parents
975 node.waiting_parents = set()
976
977 to_visit = to_visit | parents
978 pending_children = pending_children - parents
979
980 for p in parents:
981 p.ref_count = p.ref_count - 1
982 if T: T.write(self.trace_message(' removing parent %s from the pending children set\n' %
983 self.trace_node(p)))
984 except KeyError:
985
986 pass
987
988
989
990
991 self.pending_children = pending_children
992
994 """
995 Stops the current build completely.
996 """
997 self.next_candidate = self.no_next_candidate
998
1000 """
1001 Check for dependency cycles.
1002 """
1003 if not self.pending_children:
1004 return
1005
1006 nclist = [(n, find_cycle([n], set())) for n in self.pending_children]
1007
1008 genuine_cycles = [
1009 node for node,cycle in nclist
1010 if cycle or node.get_state() != NODE_EXECUTED
1011 ]
1012 if not genuine_cycles:
1013
1014
1015 return
1016
1017 desc = 'Found dependency cycle(s):\n'
1018 for node, cycle in nclist:
1019 if cycle:
1020 desc = desc + " " + " -> ".join(map(str, cycle)) + "\n"
1021 else:
1022 desc = desc + \
1023 " Internal Error: no cycle found for node %s (%s) in state %s\n" % \
1024 (node, repr(node), StateString[node.get_state()])
1025
1026 raise SCons.Errors.UserError(desc)
1027
1028
1029
1030
1031
1032
1033