Serialize calls to Node.prepare() (Anthony Roach)
[scons.git] / src / engine / SCons / Taskmaster.py
1 """SCons.Taskmaster
2
3 Generic Taskmaster.
4
5 """
6
7 #
8 # Copyright (c) 2001, 2002 Steven Knight
9 #
10 # Permission is hereby granted, free of charge, to any person obtaining
11 # a copy of this software and associated documentation files (the
12 # "Software"), to deal in the Software without restriction, including
13 # without limitation the rights to use, copy, modify, merge, publish,
14 # distribute, sublicense, and/or sell copies of the Software, and to
15 # permit persons to whom the Software is furnished to do so, subject to
16 # the following conditions:
17 #
18 # The above copyright notice and this permission notice shall be included
19 # in all copies or substantial portions of the Software.
20 #
21 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
22 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
23 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
24 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
25 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
26 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
27 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28 #
29
30 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
31
32
33
34
35 import SCons.Node
36 import string
37 import SCons.Errors
38 import copy
39
40 class Task:
41     """Default SCons build engine task.
42
43     This controls the interaction of the actual building of node
44     and the rest of the engine.
45
46     This is expected to handle all of the normally-customizable
47     aspects of controlling a build, so any given application
48     *should* be able to do what it wants by sub-classing this
49     class and overriding methods as appropriate.  If an application
50     needs to customze something by sub-classing Taskmaster (or
51     some other build engine class), we should first try to migrate
52     that functionality into this class.
53
54     Note that it's generally a good idea for sub-classes to call
55     these methods explicitly to update state, etc., rather than
56     roll their own interaction with Taskmaster from scratch."""
57     def __init__(self, tm, targets, top, node):
58         self.tm = tm
59         self.targets = targets
60         self.top = top
61         self.node = node
62
63
64     def prepare(self):
65         """Called just before the task is executed."""
66         if self.targets[0].get_state() != SCons.Node.up_to_date:
67             self.targets[0].prepare()
68
69     def execute(self):
70         """Called to execute the task.
71         
72         This methods is called from multiple threads in
73         a parallel build, so only do thread safe stuff here.
74         Do thread unsafe stuff in prepare(), executed() or failed()."""
75         if self.targets[0].get_state() != SCons.Node.up_to_date:
76             self.targets[0].build()
77
78     def get_target(self):
79         """Fetch the target being built or updated by this task.
80         """
81         return self.node
82
83     def executed(self):
84         """Called when the task has been successfully executed.
85
86         This may have been a do-nothing operation (to preserve
87         build order), so check the node's state before updating
88         things.  Most importantly, this calls back to the
89         Taskmaster to put any node tasks waiting on this one
90         back on the pending list."""
91
92         if self.targets[0].get_state() == SCons.Node.executing:
93             for t in self.targets:
94                 for side_effect in t.side_effects:
95                     side_effect.set_state(None)
96                 t.set_state(SCons.Node.executed)
97                 t.built()
98
99         self.tm.executed(self.node)
100
101     def failed(self):
102         """Default action when a task fails:  stop the build."""
103         self.fail_stop()
104
105     def fail_stop(self):
106         """Explicit stop-the-build failure."""
107         for t in self.targets:
108             t.set_state(SCons.Node.failed)
109         self.tm.stop()
110
111     def fail_continue(self):
112         """Explicit continue-the-build failure.
113
114         This sets failure status on the target nodes and all of
115         their dependent parent nodes.
116         """
117         for t in self.targets:
118             def get_parents(node, parent): return node.get_parents()
119             def set_state(node, parent): node.set_state(SCons.Node.failed)
120             walker = SCons.Node.Walker(t, get_parents, eval_func=set_state)
121             n = walker.next()
122             while n:
123                 n = walker.next()
124
125         self.tm.executed(self.node)
126
127     def make_ready(self):
128         """Make a task ready for execution."""
129         state = SCons.Node.up_to_date
130         for t in self.targets:
131             if not t.current(self.tm.calc):
132                 state = SCons.Node.executing
133         for t in self.targets:
134             if state == SCons.Node.executing:
135                 for side_effect in t.side_effects:
136                     side_effect.set_state(state)
137             t.set_state(state)
138
139 class Calc:
140     def bsig(self, node):
141         """
142         """
143         return None
144
145     def current(self, node, sig):
146         """Default SCons build engine is-it-current function.
147
148         This returns "always out of date," so every node is always
149         built/visited.
150         """
151         return 0
152
153 class Taskmaster:
154     """A generic Taskmaster for handling a bunch of targets.
155
156     Classes that override methods of this class should call
157     the base class method, so this class can do its thing.
158     """
159
160     def __init__(self, targets=[], tasker=Task, calc=Calc()):
161         self.targets = targets # top level targets
162         self.candidates = targets[:] # nodes that might be ready to be executed
163         self.candidates.reverse()
164         self.executing = [] # nodes that are currently executing
165         self.pending = [] # nodes that depend on a currently executing node
166         self.tasker = tasker
167         self.ready = None # the next task that is ready to be executed
168         self.calc = calc
169
170     def _find_next_ready_node(self):
171         """Find the next node that is ready to be built"""
172
173         if self.ready:
174             return
175
176         while self.candidates:
177             node = self.candidates[-1]
178             state = node.get_state()
179
180             # Skip nodes that have already been executed:
181             if state != None and state != SCons.Node.stack:
182                 self.candidates.pop()
183                 continue
184
185             # keep track of which nodes are in the execution stack:
186             node.set_state(SCons.Node.stack)
187
188             children = node.children()
189
190             # detect dependency cycles:
191             def in_stack(node): return node.get_state() == SCons.Node.stack
192             cycle = filter(in_stack, children)
193             if cycle:
194                 nodes = filter(in_stack, self.candidates) + cycle
195                 nodes.reverse()
196                 desc = "Dependency cycle: " + string.join(map(str, nodes), " -> ")
197                 raise SCons.Errors.UserError, desc
198
199             # Add non-derived files that have not been built
200             # to the candidates list:
201             def derived(node):
202                 return (node.builder or node.side_effect) and node.get_state() == None
203             derived = filter(derived, children)
204             if derived:
205                 derived.reverse()
206                 self.candidates.extend(derived)
207                 continue
208
209             # Skip nodes whose side-effects are currently being built:
210             cont = 0
211             for side_effect in node.side_effects:
212                 if side_effect.get_state() == SCons.Node.executing:
213                     self.pending.append(node)
214                     node.set_state(SCons.Node.pending)
215                     self.candidates.pop()
216                     cont = 1
217                     break
218             if cont: continue
219
220             # Skip nodes that are pending on a currently executing node:
221             if node.depends_on(self.executing) or node.depends_on(self.pending):
222                 self.pending.append(node)
223                 node.set_state(SCons.Node.pending)
224                 self.candidates.pop()
225                 continue
226             else:
227                 self.candidates.pop()
228                 self.ready = node
229                 break
230
231     def next_task(self):
232         """Return the next task to be executed."""
233
234         self._find_next_ready_node()
235
236         node = self.ready
237
238         if node is None:
239             return None
240
241         try:
242             tlist = node.builder.targets(node)
243         except AttributeError:
244             tlist = [node]
245         self.executing.extend(tlist)
246         self.executing.extend(node.side_effects)
247         
248         task = self.tasker(self, tlist, node in self.targets, node)
249         task.make_ready()
250         self.ready = None
251
252         return task
253
254     def is_blocked(self):
255         self._find_next_ready_node()
256
257         return not self.ready and self.pending
258
259     def stop(self):
260         """Stop the current build completely."""
261         self.candidates = []
262         self.ready = None
263         self.pending = []
264
265     def executed(self, node):
266         try:
267             tlist = node.builder.targets(node)
268         except AttributeError:
269             tlist = [node]
270         for t in tlist:
271             self.executing.remove(t)
272         for side_effect in node.side_effects:
273             self.executing.remove(side_effect)
274
275         # move the current pending nodes to the candidates list:
276         # (they may not all be ready to build, but _find_next_ready_node()
277         #  will figure out which ones are really ready)
278         for node in self.pending:
279             node.set_state(None)
280         self.pending.reverse()
281         self.candidates.extend(self.pending)
282         self.pending = []
283