9b13b60d624d25c87d6bbb2311d2bf0b121444ca
[scons.git] / src / engine / SCons / Taskmaster.py
1 """SCons.Taskmaster
2
3 Generic Taskmaster.
4
5 """
6
7 #
8 # __COPYRIGHT__
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 import string
33 import sys
34 import traceback
35
36 import SCons.Node
37 import SCons.Errors
38
39 class Task:
40     """Default SCons build engine task.
41
42     This controls the interaction of the actual building of node
43     and the rest of the engine.
44
45     This is expected to handle all of the normally-customizable
46     aspects of controlling a build, so any given application
47     *should* be able to do what it wants by sub-classing this
48     class and overriding methods as appropriate.  If an application
49     needs to customze something by sub-classing Taskmaster (or
50     some other build engine class), we should first try to migrate
51     that functionality into this class.
52
53     Note that it's generally a good idea for sub-classes to call
54     these methods explicitly to update state, etc., rather than
55     roll their own interaction with Taskmaster from scratch."""
56     def __init__(self, tm, targets, top, node):
57         self.tm = tm
58         self.targets = targets
59         self.top = top
60         self.node = node
61
62     def display(self, message):
63         """Allow the calling interface to display a message
64         """
65         pass
66
67     def prepare(self):
68         """Called just before the task is executed.
69
70         This unlinks all targets and makes all directories before
71         building anything."""
72
73         # Now that it's the appropriate time, give the TaskMaster a
74         # chance to raise any exceptions it encountered while preparing
75         # this task.
76         self.tm.exception_raise()
77
78         if self.tm.message:
79             self.display(self.tm.message)
80             self.tm.message = None
81
82         for t in self.targets:
83             t.prepare()
84
85     def execute(self):
86         """Called to execute the task.
87
88         This method is called from multiple threads in a parallel build,
89         so only do thread safe stuff here.  Do thread unsafe stuff in
90         prepare(), executed() or failed()."""
91
92         try:
93             self.targets[0].build()
94         except KeyboardInterrupt:
95             raise
96         except SystemExit:
97             raise SCons.Errors.ExplicitExit(self.targets[0], sys.exc_value.code)
98         except SCons.Errors.UserError:
99             raise
100         except SCons.Errors.BuildError:
101             raise
102         except:
103             raise SCons.Errors.BuildError(self.targets[0],
104                                           "Exception",
105                                           sys.exc_type,
106                                           sys.exc_value,
107                                           sys.exc_traceback)
108
109     def get_target(self):
110         """Fetch the target being built or updated by this task.
111         """
112         return self.node
113
114     def executed(self):
115         """Called when the task has been successfully executed.
116
117         This may have been a do-nothing operation (to preserve
118         build order), so check the node's state before updating
119         things.  Most importantly, this calls back to the
120         Taskmaster to put any node tasks waiting on this one
121         back on the pending list."""
122
123         if self.targets[0].get_state() == SCons.Node.executing:
124             for t in self.targets:
125                 for side_effect in t.side_effects:
126                     side_effect.set_state(None)
127                 t.set_state(SCons.Node.executed)
128                 t.built()
129         else:
130             for t in self.targets:
131                 t.visited()
132
133         self.tm.executed(self.node)
134
135     def failed(self):
136         """Default action when a task fails:  stop the build."""
137         self.fail_stop()
138
139     def fail_stop(self):
140         """Explicit stop-the-build failure."""
141         for t in self.targets:
142             t.set_state(SCons.Node.failed)
143         self.tm.stop()
144
145     def fail_continue(self):
146         """Explicit continue-the-build failure.
147
148         This sets failure status on the target nodes and all of
149         their dependent parent nodes.
150         """
151         for t in self.targets:
152             def get_parents(node, parent): return node.get_parents()
153             def set_state(node, parent): node.set_state(SCons.Node.failed)
154             walker = SCons.Node.Walker(t, get_parents, eval_func=set_state)
155             n = walker.next()
156             while n:
157                 n = walker.next()
158
159         self.tm.executed(self.node)
160
161     def make_ready(self):
162         """Make a task ready for execution."""
163         state = SCons.Node.up_to_date
164         calc = self.tm.calc
165         for t in self.targets:
166             c = calc or t.calculator()
167             if not t.current(c):
168                 state = SCons.Node.executing
169         for t in self.targets:
170             if state == SCons.Node.executing:
171                 for side_effect in t.side_effects:
172                     side_effect.set_state(state)
173             t.set_state(state)
174
175 def order(dependencies):
176     """Re-order a list of dependencies (if we need to)."""
177     return dependencies
178
179 class Calc:
180     def bsig(self, node):
181         """
182         """
183         return None
184
185     def current(self, node, sig):
186         """Default SCons build engine is-it-current function.
187
188         This returns "always out of date," so every node is always
189         built/visited.
190         """
191         return 0
192
193 class Taskmaster:
194     """A generic Taskmaster for handling a bunch of targets.
195
196     Classes that override methods of this class should call
197     the base class method, so this class can do its thing.
198     """
199
200     def __init__(self, targets=[], tasker=Task, calc=Calc(), order=order):
201         self.targets = targets # top level targets
202         self.candidates = targets[:] # nodes that might be ready to be executed
203         self.candidates.reverse()
204         self.executing = [] # nodes that are currently executing
205         self.pending = [] # nodes that depend on a currently executing node
206         self.tasker = tasker
207         self.ready = None # the next task that is ready to be executed
208         self.calc = calc
209         self.order = order
210         self.exception_set(None, None)
211         self.message = None
212
213     def _find_next_ready_node(self):
214         """Find the next node that is ready to be built"""
215
216         if self.ready:
217             return
218
219         while self.candidates:
220             node = self.candidates[-1]
221             state = node.get_state()
222
223             # Skip this node if it has already been executed:
224             if state != None and state != SCons.Node.stack:
225                 self.candidates.pop()
226                 continue
227
228             # Mark this node as being on the execution stack:
229             node.set_state(SCons.Node.stack)
230
231             try:
232                 children = node.children()
233             except SystemExit:
234                 e = SCons.Errors.ExplicitExit(node, sys.exc_value.code)
235                 self.exception_set(SCons.Errors.ExplicitExit, e)
236                 self.candidates.pop()
237                 self.ready = node
238                 break
239             except:
240                 # We had a problem just trying to figure out the
241                 # children (like a child couldn't be linked in to a
242                 # BuildDir, or a Scanner threw something).  Arrange to
243                 # raise the exception when the Task is "executed."
244                 x = SCons.Errors.TaskmasterException(sys.exc_type,
245                                                      sys.exc_value,
246                                                      sys.exc_traceback)
247                 self.exception_set(x)
248                 self.candidates.pop()
249                 self.ready = node
250                 break
251
252             # Detect dependency cycles:
253             def in_stack(node): return node.get_state() == SCons.Node.stack
254             cycle = filter(in_stack, children)
255             if cycle:
256                 nodes = filter(in_stack, self.candidates) + cycle
257                 nodes.reverse()
258                 desc = "Dependency cycle: " + string.join(map(str, nodes), " -> ")
259                 raise SCons.Errors.UserError, desc
260
261             # Find all of the derived dependencies (that is,
262             # children who have builders or are side effects):
263             try:
264                 def derived_nodes(node): return node.is_derived() or node.is_pseudo_derived()
265                 derived = filter(derived_nodes, children)
266             except:
267                 # We had a problem just trying to figure out if any of
268                 # the kids are derived (like a child couldn't be linked
269                 # from a repository).  Arrange to raise the exception
270                 # when the Task is "executed."
271                 x = SCons.Errors.TaskmasterException(sys.exc_type,
272                                                      sys.exc_value,
273                                                      sys.exc_traceback)
274                 self.exception_set(x)
275                 self.candidates.pop()
276                 self.ready = node
277                 break
278
279             # If there aren't any children with builders and this
280             # was a top-level argument, then see if we can find any
281             # corresponding targets in linked build directories:
282             if not derived and node in self.targets:
283                 alt, message = node.alter_targets()
284                 if alt:
285                     self.message = message
286                     self.candidates.pop()
287                     self.candidates.extend(alt)
288                     continue
289
290             # Add derived files that have not been built
291             # to the candidates list:
292             def unbuilt_nodes(node): return node.get_state() == None
293             not_built = filter(unbuilt_nodes, derived)
294             if not_built:
295                 not_built.reverse()
296                 self.candidates.extend(self.order(not_built))
297                 continue
298
299             # Skip this node if it has side-effects that are
300             # currently being built:
301             cont = 0
302             for side_effect in node.side_effects:
303                 if side_effect.get_state() == SCons.Node.executing:
304                     self.pending.append(node)
305                     node.set_state(SCons.Node.pending)
306                     self.candidates.pop()
307                     cont = 1
308                     break
309             if cont: continue
310
311             # Skip this node if it is pending on a currently
312             # executing node:
313             if node.depends_on(self.executing) or node.depends_on(self.pending):
314                 self.pending.append(node)
315                 node.set_state(SCons.Node.pending)
316                 self.candidates.pop()
317                 continue
318
319             # The default when we've gotten through all of the checks above:
320             # this node is ready to be built.
321             self.candidates.pop()
322             self.ready = node
323             break
324
325     def next_task(self):
326         """Return the next task to be executed."""
327
328         self._find_next_ready_node()
329
330         node = self.ready
331
332         if node is None:
333             return None
334
335         try:
336             tlist = node.builder.targets(node)
337         except AttributeError:
338             tlist = [node]
339         self.executing.extend(tlist)
340         self.executing.extend(node.side_effects)
341         
342         task = self.tasker(self, tlist, node in self.targets, node)
343         try:
344             task.make_ready()
345         except:
346             # We had a problem just trying to get this task ready (like
347             # a child couldn't be linked in to a BuildDir when deciding
348             # whether this node is current).  Arrange to raise the
349             # exception when the Task is "executed."
350             x = SCons.Errors.TaskmasterException(sys.exc_type,
351                                                  sys.exc_value,
352                                                  sys.exc_traceback)
353             self.exception_set(x)
354         self.ready = None
355
356         return task
357
358     def is_blocked(self):
359         self._find_next_ready_node()
360
361         return not self.ready and self.pending
362
363     def stop(self):
364         """Stop the current build completely."""
365         self.candidates = []
366         self.ready = None
367         self.pending = []
368
369     def executed(self, node):
370         try:
371             tlist = node.builder.targets(node)
372         except AttributeError:
373             tlist = [node]
374         for t in tlist:
375             self.executing.remove(t)
376         for side_effect in node.side_effects:
377             self.executing.remove(side_effect)
378
379         # move the current pending nodes to the candidates list:
380         # (they may not all be ready to build, but _find_next_ready_node()
381         #  will figure out which ones are really ready)
382         for node in self.pending:
383             node.set_state(None)
384         self.pending.reverse()
385         self.candidates.extend(self.pending)
386         self.pending = []
387
388     def exception_set(self, type, value=None):
389         """Record an exception type and value to raise later, at an
390         appropriate time."""
391         self.exc_type = type
392         self.exc_value = value
393         self.exc_traceback = traceback
394
395     def exception_raise(self):
396         """Raise any pending exception that was recorded while
397         getting a Task ready for execution."""
398         if self.exc_type:
399             try:
400                 try:
401                     raise self.exc_type, self.exc_value
402                 except TypeError:
403                     # exc_type was probably an instance,
404                     # so raise it by itself.
405                     raise self.exc_type
406             finally:
407                 self.exception_set(None, None)