~ubuntu-branches/ubuntu/saucy/xmms2/saucy-proposed

« back to all changes in this revision

Viewing changes to wafadmin/Task.py

  • Committer: Bazaar Package Importer
  • Author(s): Florian Ragwitz
  • Date: 2009-05-02 08:31:32 UTC
  • mto: (1.1.6 upstream) (6.1.3 sid)
  • mto: This revision was merged to the branch mainline in revision 23.
  • Revision ID: james.westby@ubuntu.com-20090502083132-y0ulwiqbk4lxfd4z
ImportĀ upstreamĀ versionĀ 0.6DrMattDestruction

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#! /usr/bin/env python
 
1
#!/usr/bin/env python
2
2
# encoding: utf-8
3
 
# Thomas Nagy, 2005 (ita)
4
 
 
5
 
"Atomic operations that create nodes or execute commands"
6
 
 
7
 
import os, types, shutil
8
 
try: from hashlib import md5
9
 
except ImportError: from md5 import md5
10
 
 
11
 
import Params, Scan, Action, Runner, Object
12
 
from Params import debug, error, warning
13
 
 
14
 
g_tasks_done    = []
15
 
"tasks that have been run, this is used in tests to check which tasks were actually launched"
16
 
 
17
 
g_default_param = {'path_lst':[]}
18
 
"the default scanner parameter"
19
 
 
20
 
class TaskManager:
21
 
        """There is a single instance of TaskManager held by Task.py:g_tasks
22
 
        The manager holds a list of TaskGroup
23
 
        Each TaskGroup contains a map(priority, list of tasks)"""
 
3
# Thomas Nagy, 2005-2008 (ita)
 
4
 
 
5
"""
 
6
Running tasks in parallel is a simple problem, but in practice it is more complicated:
 
7
* dependencies discovered during the build (dynamic task creation)
 
8
* dependencies discovered after files are compiled
 
9
* the amount of tasks and dependencies (graph size) can be huge
 
10
 
 
11
This is why the dependency management is split on three different levels:
 
12
1. groups of tasks that run all after another group of tasks
 
13
2. groups of tasks that can be run in parallel
 
14
3. tasks that can run in parallel, but with possible unknown ad-hoc dependencies
 
15
 
 
16
The point #1 represents a strict sequential order between groups of tasks, for example a compiler is produced
 
17
and used to compile the rest, whereas #2 and #3 represent partial order constraints where #2 applies to the kind of task
 
18
and #3 applies to the task instances.
 
19
 
 
20
#1 is held by the task manager: ordered list of TaskGroups (see bld.add_group)
 
21
#2 is held by the task groups and the task types: precedence after/before (topological sort),
 
22
   and the constraints extracted from file extensions
 
23
#3 is held by the tasks individually (attribute run_after),
 
24
   and the scheduler (Runner.py) use Task::runnable_status to reorder the tasks
 
25
 
 
26
--
 
27
 
 
28
To try, use something like this in your code:
 
29
import Constants, Task
 
30
Task.algotype = Constants.MAXPARALLEL
 
31
 
 
32
--
 
33
 
 
34
There are two concepts with the tasks (individual units of change):
 
35
* dependency (if 1 is recompiled, recompile 2)
 
36
* order (run 2 after 1)
 
37
 
 
38
example 1: if t1 depends on t2 and t2 depends on t3 it is not necessary to make t1 depend on t3 (dependency is transitive)
 
39
example 2: if t1 depends on a node produced by t2, it is not immediately obvious that t1 must run after t2 (order is not obvious)
 
40
 
 
41
The role of the Task Manager is to give the tasks in order (groups of task that may be run in parallel one after the other)
 
42
 
 
43
"""
 
44
 
 
45
import os, types, shutil, sys, re, random, time
 
46
from Utils import md5
 
47
import Build, Runner, Utils, Node, Logs, Options
 
48
from Logs import debug
 
49
from Constants import *
 
50
 
 
51
algotype = NORMAL
 
52
#algotype = JOBCONTROL
 
53
#algotype = MAXPARALLEL
 
54
 
 
55
"""
 
56
Enable different kind of dependency algorithms:
 
57
1 make groups: first compile all cpps and then compile all links (NORMAL)
 
58
2 parallelize all (each link task run after its dependencies) (MAXPARALLEL)
 
59
3 like 1 but provide additional constraints for the parallelization (MAXJOBS)
 
60
 
 
61
In theory 1. will be faster than 2 for waf, but might be slower for builds
 
62
The scheme 2 will not allow for running tasks one by one so it can cause disk thrashing on huge builds
 
63
 
 
64
"""
 
65
 
 
66
FILE_DEPS = False
 
67
def file_deps(tasks):
 
68
        """Infer dependencies from task input and output nodes
 
69
        """
 
70
 
 
71
        v = {}
 
72
        for x in tasks:
 
73
                try:
 
74
                        (ins, outs) = v[x.env.variant()]
 
75
                except KeyError:
 
76
                        ins = {}
 
77
                        outs = {}
 
78
                        v[x.env.variant()] = (ins, outs)
 
79
 
 
80
                for a in getattr(x, 'inputs', []):
 
81
                        try: ins[a.id].append(x)
 
82
                        except KeyError: ins[a.id] = [x]
 
83
                for a in getattr(x, 'outputs', []):
 
84
                        try: outs[a.id].append(x)
 
85
                        except KeyError: outs[a.id] = [x]
 
86
 
 
87
        for (ins, outs) in v.values():
 
88
                links = set(ins.iterkeys()).intersection(outs.iterkeys())
 
89
                for k in links:
 
90
                        for a in ins[k]:
 
91
                                for b in outs[k]:
 
92
                                        a.set_run_after(b)
 
93
 
 
94
class TaskManager(object):
 
95
        """The manager is attached to the build object, it holds a list of TaskGroup"""
24
96
        def __init__(self):
25
97
                self.groups = []
26
 
                self.idx    = 0
27
 
        def add_group(self, name=''):
28
 
                if not name:
29
 
                        try: size = len(self.groups)
30
 
                        except: size = 0
31
 
                        name = 'group-%d' % size
32
 
                if not self.groups:
33
 
                        self.groups = [TaskGroup(name)]
34
 
                        return
35
 
                if not self.groups[0].prio:
36
 
                        warning('add_group: an empty group is already present')
37
 
                        return
38
 
                self.groups = self.groups + [TaskGroup(name)]
39
 
        def add_task(self, task, prio):
40
 
                if not self.groups: self.add_group('group-0')
41
 
                task.m_idx = self.idx
42
 
                self.idx += 1
43
 
                self.groups[-1].add_task(task, prio)
 
98
                self.tasks_done = []
 
99
                self.current_group = 0
 
100
 
 
101
        def get_next_set(self):
 
102
                """return the next set of tasks to execute
 
103
                the first parameter is the maximum amount of parallelization that may occur"""
 
104
                ret = None
 
105
                while not ret and self.current_group < len(self.groups):
 
106
                        ret = self.groups[self.current_group].get_next_set()
 
107
                        if ret: return ret
 
108
                        else: self.current_group += 1
 
109
                return (None, None)
 
110
 
 
111
        def add_group(self):
 
112
                if self.groups and not self.groups[0].tasks:
 
113
                        warn('add_group: an empty group is already present')
 
114
                self.groups.append(TaskGroup())
 
115
 
 
116
        def add_task(self, task):
 
117
                if not self.groups: self.add_group()
 
118
                self.groups[-1].add_task(task)
 
119
 
44
120
        def total(self):
45
121
                total = 0
46
122
                if not self.groups: return 0
47
123
                for group in self.groups:
48
 
                        for p in group.prio:
49
 
                                total += len(group.prio[p])
 
124
                        total += len(group.tasks)
50
125
                return total
51
 
        def debug(self):
52
 
                for i in self.groups:
53
 
                        print "-----group-------", i.name
54
 
                        for j in i.prio:
55
 
                                print "prio: ", j, str(i.prio[j])
56
 
 
57
 
"the container of all tasks (instance of TaskManager)"
58
 
g_tasks = TaskManager()
59
 
 
60
 
class TaskGroup:
61
 
        "A TaskGroup maps priorities (integers) to lists of tasks"
62
 
        def __init__(self, name):
63
 
                self.name = name
64
 
                self.info = ''
65
 
                self.prio = {} # map priority numbers to tasks
66
 
        def add_task(self, task, prio):
67
 
                try: self.prio[prio].append(task)
68
 
                except: self.prio[prio] = [task]
69
 
 
70
 
class TaskBase:
71
 
        "TaskBase is the base class for task objects"
72
 
        def __init__(self, priority, normal=1):
73
 
                self.m_display = ''
74
 
                self.m_hasrun=0
75
 
                global g_tasks
76
 
                if normal:
77
 
                        # add to the list of tasks
78
 
                        g_tasks.add_task(self, priority)
 
126
 
 
127
        def add_finished(self, tsk):
 
128
                self.tasks_done.append(tsk)
 
129
                bld = tsk.generator.bld
 
130
                if Options.is_install:
 
131
                        f = None
 
132
                        if 'install' in tsk.__dict__:
 
133
                                f = tsk.__dict__['install']
 
134
                                f(tsk)
 
135
                        else:
 
136
                                tsk.install()
 
137
 
 
138
class TaskGroup(object):
 
139
        "the compilation of one group does not begin until the previous group has finished (in the manager)"
 
140
        def __init__(self):
 
141
                self.tasks = [] # this list will be consumed
 
142
 
 
143
                self.cstr_groups = {} # tasks having equivalent constraints
 
144
                self.cstr_order = {} # partial order between the cstr groups
 
145
                self.temp_tasks = [] # tasks put on hold
 
146
                self.ready = 0
 
147
 
 
148
        def reset(self):
 
149
                "clears the state of the object (put back the tasks into self.tasks)"
 
150
                for x in self.cstr_groups:
 
151
                        self.tasks += self.cstr_groups[x]
 
152
                self.tasks = self.temp_tasks + self.tasks
 
153
                self.temp_tasks = []
 
154
                self.cstr_groups = []
 
155
                self.cstr_order = {}
 
156
                self.ready = 0
 
157
 
 
158
        def prepare(self):
 
159
                "prepare the scheduling"
 
160
                self.ready = 1
 
161
                if FILE_DEPS:
 
162
                        file_deps(self.tasks)
 
163
                self.make_cstr_groups()
 
164
                self.extract_constraints()
 
165
 
 
166
        def get_next_set(self):
 
167
                "next list of tasks to execute using max job settings, returns (maxjobs, task_list)"
 
168
                global algotype
 
169
                if algotype == NORMAL:
 
170
                        tasks = self.tasks_in_parallel()
 
171
                        maxj = MAXJOBS
 
172
                elif algotype == JOBCONTROL:
 
173
                        (maxj, tasks) = self.tasks_by_max_jobs()
 
174
                elif algotype == MAXPARALLEL:
 
175
                        tasks = self.tasks_with_inner_constraints()
 
176
                        maxj = MAXJOBS
79
177
                else:
80
 
                        self.m_idx = g_tasks.idx
81
 
                        g_tasks.idx += 1
82
 
        def may_start(self):
83
 
                "return non-zero if the task may is ready"
84
 
                return 1
85
 
        def must_run(self):
86
 
                "return 0 if the task does not need to run"
87
 
                return 1
88
 
        def prepare(self):
89
 
                "prepare the task for further processing"
90
 
                pass
91
 
        def update_stat(self):
 
178
                        raise Utils.WafError("unknown algorithm type %s" % (algotype))
 
179
 
 
180
                if not tasks: return ()
 
181
                return (maxj, tasks)
 
182
 
 
183
        def make_cstr_groups(self):
 
184
                "unite the tasks that have similar constraints"
 
185
                self.cstr_groups = {}
 
186
                for x in self.tasks:
 
187
                        h = x.hash_constraints()
 
188
                        try: self.cstr_groups[h].append(x)
 
189
                        except KeyError: self.cstr_groups[h] = [x]
 
190
 
 
191
        def add_task(self, task):
 
192
                try: self.tasks.append(task)
 
193
                except KeyError: self.tasks = [task]
 
194
 
 
195
        def set_order(self, a, b):
 
196
                try: self.cstr_order[a].add(b)
 
197
                except KeyError: self.cstr_order[a] = set([b,])
 
198
 
 
199
        def compare_exts(self, t1, t2):
 
200
                "extension production"
 
201
                x = "ext_in"
 
202
                y = "ext_out"
 
203
                in_ = t1.attr(x, ())
 
204
                out_ = t2.attr(y, ())
 
205
                for k in in_:
 
206
                        if k in out_:
 
207
                                return -1
 
208
                in_ = t2.attr(x, ())
 
209
                out_ = t1.attr(y, ())
 
210
                for k in in_:
 
211
                        if k in out_:
 
212
                                return 1
 
213
                return 0
 
214
 
 
215
        def compare_partial(self, t1, t2):
 
216
                "partial relations after/before"
 
217
                m = "after"
 
218
                n = "before"
 
219
                name = t2.__class__.__name__
 
220
                if name in t1.attr(m, ()): return -1
 
221
                elif name in t1.attr(n, ()): return 1
 
222
                name = t1.__class__.__name__
 
223
                if name in t2.attr(m, ()): return 1
 
224
                elif name in t2.attr(n, ()): return -1
 
225
                return 0
 
226
 
 
227
        def extract_constraints(self):
 
228
                "extract the parallelization constraints from the tasks with different constraints"
 
229
                keys = self.cstr_groups.keys()
 
230
                max = len(keys)
 
231
                # hopefully the length of this list is short
 
232
                for i in xrange(max):
 
233
                        t1 = self.cstr_groups[keys[i]][0]
 
234
                        for j in xrange(i + 1, max):
 
235
                                t2 = self.cstr_groups[keys[j]][0]
 
236
 
 
237
                                # add the constraints based on the comparisons
 
238
                                val = (self.compare_exts(t1, t2)
 
239
                                        or self.compare_partial(t1, t2)
 
240
                                        )
 
241
                                if val > 0:
 
242
                                        self.set_order(keys[i], keys[j])
 
243
                                elif val < 0:
 
244
                                        self.set_order(keys[j], keys[i])
 
245
 
 
246
                # TODO: extract constraints by file extensions on the actions
 
247
 
 
248
        def tasks_in_parallel(self):
 
249
                "(NORMAL) next list of tasks that may be executed in parallel"
 
250
 
 
251
                if not self.ready: self.prepare()
 
252
 
 
253
                keys = self.cstr_groups.keys()
 
254
 
 
255
                unconnected = []
 
256
                remainder = []
 
257
 
 
258
                for u in keys:
 
259
                        for k in self.cstr_order.values():
 
260
                                if u in k:
 
261
                                        remainder.append(u)
 
262
                                        break
 
263
                        else:
 
264
                                unconnected.append(u)
 
265
 
 
266
                toreturn = []
 
267
                for y in unconnected:
 
268
                        toreturn.extend(self.cstr_groups[y])
 
269
 
 
270
                # remove stuff only after
 
271
                for y in unconnected:
 
272
                                try: self.cstr_order.__delitem__(y)
 
273
                                except KeyError: pass
 
274
                                self.cstr_groups.__delitem__(y)
 
275
 
 
276
                if not toreturn and remainder:
 
277
                        raise Utils.WafError("circular order constraint detected %r" % remainder)
 
278
 
 
279
                return toreturn
 
280
 
 
281
        def tasks_by_max_jobs(self):
 
282
                "(JOBCONTROL) returns the tasks that can run in parallel with the max amount of jobs"
 
283
                if not self.ready: self.prepare()
 
284
                if not self.temp_tasks: self.temp_tasks = self.tasks_in_parallel()
 
285
                if not self.temp_tasks: return (None, None)
 
286
 
 
287
                maxjobs = MAXJOBS
 
288
                ret = []
 
289
                remaining = []
 
290
                for t in self.temp_tasks:
 
291
                        m = getattr(t, "maxjobs", getattr(self.__class__, "maxjobs", MAXJOBS))
 
292
                        if m > maxjobs:
 
293
                                remaining.append(t)
 
294
                        elif m < maxjobs:
 
295
                                remaining += ret
 
296
                                ret = [t]
 
297
                                maxjobs = m
 
298
                        else:
 
299
                                ret.append(t)
 
300
                self.temp_tasks = remaining
 
301
                return (maxjobs, ret)
 
302
 
 
303
        def tasks_with_inner_constraints(self):
 
304
                """(MAXPARALLEL) returns all tasks in this group, but add the constraints on each task instance
 
305
                as an optimization, it might be desirable to discard the tasks which do not have to run"""
 
306
                if not self.ready: self.prepare()
 
307
 
 
308
                if getattr(self, "done", None): return None
 
309
 
 
310
                for p in self.cstr_order:
 
311
                        for v in self.cstr_order[p]:
 
312
                                for m in self.cstr_groups[p]:
 
313
                                        for n in self.cstr_groups[v]:
 
314
                                                n.set_run_after(m)
 
315
                self.cstr_order = {}
 
316
                self.cstr_groups = {}
 
317
                self.done = 1
 
318
                return self.tasks[:] # make a copy
 
319
 
 
320
class store_task_type(type):
 
321
        "store the task types that have a name ending in _task into a map (remember the existing task types)"
 
322
        def __init__(cls, name, bases, dict):
 
323
                super(store_task_type, cls).__init__(name, bases, dict)
 
324
                name = cls.__name__
 
325
 
 
326
                if name.endswith('_task'):
 
327
                        name = name.replace('_task', '')
 
328
                        TaskBase.classes[name] = cls
 
329
 
 
330
class TaskBase(object):
 
331
        """Base class for all Waf tasks
 
332
 
 
333
        The most important methods are (by usual order of call):
 
334
        1 runnable_status: ask the task if it should be run, skipped, or if we have to ask later
 
335
        2 __str__: string to display to the user
 
336
        3 run: execute the task
 
337
        4 post_run: after the task is run, update the cache about the task
 
338
 
 
339
        This class should be seen as an interface, it provides the very minimum necessary for the scheduler
 
340
        so it does not do much.
 
341
 
 
342
        For illustration purposes, TaskBase instances try to execute self.fun (if provided)
 
343
        """
 
344
 
 
345
        __metaclass__ = store_task_type
 
346
 
 
347
        color = "GREEN"
 
348
        maxjobs = MAXJOBS
 
349
        classes = {}
 
350
        stat = None
 
351
 
 
352
        def __init__(self, *k, **kw):
 
353
                self.hasrun = NOT_RUN
 
354
 
 
355
                try:
 
356
                        self.generator = kw['generator']
 
357
                except KeyError:
 
358
                        self.generator = self
 
359
                        self.bld = Build.bld
 
360
 
 
361
                if kw.get('normal', 1):
 
362
                        self.generator.bld.task_manager.add_task(self)
 
363
 
 
364
        def __repr__(self):
 
365
                "used for debugging"
 
366
                return '\n\t{task: %s %s}' % (self.__class__.__name__, str(getattr(self, "fun", "")))
 
367
 
 
368
        def __str__(self):
 
369
                "string to display to the user"
 
370
                if hasattr(self, 'fun'):
 
371
                        return 'executing: %s\n' % self.fun.__name__
 
372
                return self.__class__.__name__ + '\n'
 
373
 
 
374
        def runnable_status(self):
 
375
                "RUN_ME SKIP_ME or ASK_LATER"
 
376
                return RUN_ME
 
377
 
 
378
        def can_retrieve_cache(self):
 
379
                return False
 
380
 
 
381
        def call_run(self):
 
382
                if self.can_retrieve_cache():
 
383
                        return 0
 
384
                return self.run()
 
385
 
 
386
        def run(self):
 
387
                "called if the task must run"
 
388
                if hasattr(self, 'fun'):
 
389
                        return self.fun(self)
 
390
                return 0
 
391
 
 
392
        def post_run(self):
92
393
                "update the dependency tree (node stats)"
93
394
                pass
94
 
        def debug_info(self):
95
 
                "return debug info"
96
 
                return ''
97
 
        def debug(self):
98
 
                "prints the debug info"
99
 
                pass
100
 
        def run(self):
101
 
                "process the task"
102
 
                pass
103
 
        def color(self):
104
 
                "return the color to use for the console messages"
105
 
                return 'BLUE'
106
 
        def set_display(self, v):
107
 
                self.m_display = v
108
 
        def get_display(self):
109
 
                return self.m_display
 
395
 
 
396
        def display(self):
 
397
                "print either the description (using __str__) or the progress bar or the ide output"
 
398
                col1 = Logs.colors(self.color)
 
399
                col2 = Logs.colors.NORMAL
 
400
 
 
401
                if Options.options.progress_bar == 1:
 
402
                        return self.generator.bld.progress_line(self.position[0], self.position[1], col1, col2)
 
403
 
 
404
                if Options.options.progress_bar == 2:
 
405
                        ini = self.generator.bld.ini
 
406
                        ela = time.strftime('%H:%M:%S', time.gmtime(time.time() - ini))
 
407
                        try:
 
408
                                ins  = ','.join([n.name for n in self.inputs])
 
409
                        except AttributeError:
 
410
                                ins = ''
 
411
                        try:
 
412
                                outs = ','.join([n.name for n in self.outputs])
 
413
                        except AttributeError:
 
414
                                outs = ''
 
415
                        return '|Total %s|Current %s|Inputs %s|Outputs %s|Time %s|\n' % (self.position[1], self.position[0], ins, outs, ela)
 
416
 
 
417
                total = self.position[1]
 
418
                n = len(str(total))
 
419
                fs = '[%%%dd/%%%dd] %%s%%s%%s' % (n, n)
 
420
                return fs % (self.position[0], self.position[1], col1, str(self), col2)
 
421
 
 
422
        def attr(self, att, default=None):
 
423
                "retrieve an attribute from the instance or from the class (microoptimization here)"
 
424
                ret = getattr(self, att, self)
 
425
                if ret is self: return getattr(self.__class__, att, default)
 
426
                return ret
 
427
 
 
428
        def hash_constraints(self):
 
429
                "identify a task type for all the constraints relevant for the scheduler: precedence, file production"
 
430
                a = self.attr
 
431
                sum = hash((self.__class__.__name__,
 
432
                        str(a('before', '')),
 
433
                        str(a('after', '')),
 
434
                        str(a('ext_in', '')),
 
435
                        str(a('ext_out', '')),
 
436
                        self.__class__.maxjobs))
 
437
                return sum
 
438
 
 
439
        def format_error(self):
 
440
                "error message to display to the user (when a build fails)"
 
441
                if getattr(self, "error_msg", None):
 
442
                        return self.error_msg
 
443
                elif self.hasrun == CRASHED:
 
444
                        try:
 
445
                                return " -> task failed (err #%d): %r" % (self.err_code, self)
 
446
                        except AttributeError:
 
447
                                return " -> task failed: %r" % self
 
448
                elif self.hasrun == EXCEPTION:
 
449
                        return self.err_msg
 
450
                elif self.hasrun == MISSING:
 
451
                        return " -> missing files: %r" % self
 
452
                else:
 
453
                        return ''
 
454
 
 
455
        def install(self):
 
456
                """
 
457
                installation is performed by looking at the task attributes:
 
458
                * install_path: installation path like "${PREFIX}/bin"
 
459
                * filename: install the first node in the outputs as a file with a particular name, be certain to give os.sep
 
460
                * chmod: permissions
 
461
                """
 
462
                bld = self.generator.bld
 
463
                d = self.attr('install')
 
464
 
 
465
                if self.attr('install_path'):
 
466
                        lst = [a.relpath_gen(bld.srcnode) for a in self.outputs]
 
467
                        perm = self.attr('chmod', O644)
 
468
                        if self.attr('src'):
 
469
                                # if src is given, install the sources too
 
470
                                lst += [a.relpath_gen(bld.srcnode) for a in self.inputs]
 
471
                        if self.attr('filename'):
 
472
                                dir = self.install_path + self.attr('filename')
 
473
                                bld.install_as(self.install_path, lst[0], self.env, perm)
 
474
                        else:
 
475
                                bld.install_files(self.install_path, lst, self.env, perm)
110
476
 
111
477
class Task(TaskBase):
112
 
        "Task is the more common task. It has input nodes and output nodes"
113
 
        def __init__(self, action_name, env, priority=5, normal=1):
114
 
                TaskBase.__init__(self, priority, normal)
115
 
 
116
 
                # name of the action associated to this task type
117
 
                self.m_action = Action.g_actions[action_name]
118
 
 
119
 
                # environment in use
120
 
                self.m_env = env
 
478
        """The parent class is quite limited, in this version:
 
479
        * file system interaction: input and output nodes
 
480
        * persistence: do not re-execute tasks that have already run
 
481
        * caching: same files can be saved and retrieved from a cache directory
 
482
        * dependencies:
 
483
           implicit, like .c files depending on .h files
 
484
       explicit, like the input nodes or the dep_nodes
 
485
       environment variables, like the CXXFLAGS in self.env
 
486
        """
 
487
        vars = []
 
488
        def __init__(self, env, **kw):
 
489
                TaskBase.__init__(self, **kw)
 
490
                self.env = env
121
491
 
122
492
                # inputs and outputs are nodes
123
493
                # use setters when possible
124
 
                self.m_inputs  = []
125
 
                self.m_outputs = []
126
 
 
127
 
                # scanner function
128
 
                self.m_scanner        = Scan.g_default_scanner
129
 
 
130
 
                # TODO get rid of this:
131
 
                # default scanner parameter
132
 
                global g_default_param
133
 
                self.m_scanner_params = g_default_param
134
 
 
135
 
                # additionally, you may define the following
136
 
                # self.dep_vars = 'some_env_var'
137
 
 
 
494
                self.inputs  = []
 
495
                self.outputs = []
 
496
 
 
497
                self.deps_nodes = []
 
498
                self.run_after = []
 
499
 
 
500
                # Additionally, you may define the following
 
501
                #self.dep_vars  = 'PREFIX DATADIR'
 
502
 
 
503
        def __str__(self):
 
504
                "string to display to the user"
 
505
                env = self.env
 
506
                src_str = ' '.join([a.nice_path(env) for a in self.inputs])
 
507
                tgt_str = ' '.join([a.nice_path(env) for a in self.outputs])
 
508
                if self.outputs: sep = ' -> '
 
509
                else: sep = ''
 
510
                return '%s: %s%s%s\n' % (self.__class__.__name__, src_str, sep, tgt_str)
 
511
 
 
512
        def __repr__(self):
 
513
                return "".join(['\n\t{task: ', self.__class__.__name__, " ", ",".join([x.name for x in self.inputs]), " -> ", ",".join([x.name for x in self.outputs]), '}'])
 
514
 
 
515
        def unique_id(self):
 
516
                "get a unique id: hash the node paths, the variant, the class, the function"
 
517
                try:
 
518
                        return self.uid
 
519
                except AttributeError:
 
520
                        m = md5()
 
521
                        up = m.update
 
522
                        up(self.env.variant())
 
523
                        for x in self.inputs + self.outputs:
 
524
                                up(x.abspath())
 
525
                        up(self.__class__.__name__)
 
526
                        up(Utils.h_fun(self.run))
 
527
                        self.uid = m.digest()
 
528
                        return self.uid
138
529
 
139
530
        def set_inputs(self, inp):
140
 
                if type(inp) is types.ListType: self.m_inputs = inp
141
 
                else: self.m_inputs = [inp]
 
531
                if type(inp) is types.ListType: self.inputs += inp
 
532
                else: self.inputs.append(inp)
142
533
 
143
534
        def set_outputs(self, out):
144
 
                if type(out) is types.ListType: self.m_outputs = out
145
 
                else: self.m_outputs = [out]
 
535
                if type(out) is types.ListType: self.outputs += out
 
536
                else: self.outputs.append(out)
146
537
 
147
538
        def set_run_after(self, task):
148
 
                "set (scheduler) dependency on another task"
 
539
                "set (scheduler) order on another task"
149
540
                # TODO: handle list or object
150
541
                assert isinstance(task, TaskBase)
151
 
                try: self.m_run_after.append(task)
152
 
                except AttributeError: self.m_run_after = [task]
153
 
 
154
 
        def get_run_after(self):
155
 
                try: return self.m_run_after
156
 
                except AttributeError: return []
 
542
                self.run_after.append(task)
157
543
 
158
544
        def add_file_dependency(self, filename):
159
545
                "TODO user-provided file dependencies"
160
 
                node = Params.g_build.m_current.find_build(filename)
161
 
                try: self.m_deps_nodes.append(node)
162
 
                except: self.m_deps_nodes = [node]
163
 
 
164
 
        #------------ users are probably less interested in the following methods --------------#
 
546
                node = self.generator.bld.current.find_resource(filename)
 
547
                self.deps_nodes.append(node)
165
548
 
166
549
        def signature(self):
167
 
                # compute the result one time, and suppose the scanner.get_signature will give the good result
168
 
                try: return self._sign_all
 
550
                # compute the result one time, and suppose the scan_signature will give the good result
 
551
                try: return self.cache_sig[0]
169
552
                except AttributeError: pass
170
553
 
171
 
                tree = Params.g_build
172
 
 
173
554
                m = md5()
174
555
 
175
 
                dep_sig = Params.sig_nil
176
 
                scan = None
177
 
                try:
178
 
                        scan = self.m_scanner
179
 
                except AttributeError: # there is no scanner for the task
180
 
                        for x in self.m_inputs:
181
 
                                variant = x.variant(self.m_env)
182
 
                                v = tree.m_tstamp_variants[variant][x]
183
 
                                dep_sig = hash( (dep_sig, v) )
184
 
                                m.update(v)
185
 
                if scan:
186
 
                        dep_sig = scan.get_signature(self)
187
 
                        m.update(dep_sig)
188
 
 
189
 
                act_sig = None
190
 
                try: act_sig = self.m_action.signature(self)
191
 
                except AttributeError: act_sig = Object.sign_env_vars(self.m_env, self.m_action.m_vars)
192
 
                m.update(act_sig)
193
 
 
194
 
                var_sig = None
195
 
                try:
196
 
                        var_sig = Object.sign_env_vars(self.m_env, self.dep_vars)
197
 
                        m.update(var_sig)
198
 
                except AttributeError:
199
 
                        pass
200
 
 
201
 
                node_sig = Params.sig_nil
202
 
                try:
203
 
                        for x in self.dep_nodes:
204
 
                                variant = x.variant(self.m_env)
205
 
                                v = tree.m_tstamp_variants[variant][x]
206
 
                                node_sig = hash( (node_sig, v) )
207
 
                                m.update(v)
208
 
                except AttributeError:
209
 
                        pass
210
 
 
211
 
                # hash additional node dependencies
 
556
                # explicit deps
 
557
                exp_sig = self.sig_explicit_deps()
 
558
                m.update(exp_sig)
 
559
 
 
560
                # implicit deps
 
561
                imp_sig = self.scan and self.sig_implicit_deps() or SIG_NIL
 
562
                m.update(imp_sig)
 
563
 
 
564
                # env vars
 
565
                var_sig = self.sig_vars()
 
566
                m.update(var_sig)
 
567
 
 
568
                # we now have the signature (first element) and the details (for debugging)
212
569
                ret = m.digest()
213
 
 
214
 
                self.cache_sig = [ret, dep_sig, act_sig, var_sig, node_sig]
215
 
 
216
 
                self._sign_all = ret
 
570
                self.cache_sig = (ret, exp_sig, imp_sig, var_sig)
217
571
                return ret
218
572
 
219
 
        def may_start(self):
220
 
                "wait for other tasks to complete"
221
 
                if (not self.m_inputs) or (not self.m_outputs):
222
 
                        if not (not self.m_inputs) and (not self.m_outputs):
223
 
                                error("potentially grave error, task is invalid : no inputs or outputs")
224
 
                                self.debug()
225
 
 
226
 
                # the scanner has its word to say
227
 
                try:
228
 
                        if not self.m_scanner.may_start(self):
229
 
                                return 1
230
 
                except AttributeError:
231
 
                        pass
232
 
 
233
 
                # this is a dependency using the scheduler, as opposed to hash-based ones
234
 
                for t in self.get_run_after():
235
 
                        if not t.m_hasrun:
236
 
                                return 0
237
 
                return 1
238
 
 
239
 
        def must_run(self):
240
 
                "see if the task must be run or not"
 
573
        def runnable_status(self):
 
574
                "SKIP_ME RUN_ME or ASK_LATER"
241
575
                #return 0 # benchmarking
242
576
 
243
 
                tree = Params.g_build
244
 
                ret = 0
245
 
 
246
 
                # for tasks that have no inputs or outputs and are run all the time
247
 
                if not self.m_inputs and not self.m_outputs:
248
 
                        self.m_dep_sig = Params.sig_nil
249
 
                        return 1
 
577
                if self.inputs and (not self.outputs):
 
578
                        if not getattr(self.__class__, 'quiet', None):
 
579
                                warn("task is probably invalid (no inputs OR outputs): override in a Task subclass or set the attribute 'quiet' %r" % self)
 
580
 
 
581
                for t in self.run_after:
 
582
                        if not t.hasrun:
 
583
                                return ASK_LATER
 
584
 
 
585
                env = self.env
 
586
                bld = self.generator.bld
250
587
 
251
588
                # look at the previous signature first
 
589
                time = None
 
590
                for node in self.outputs:
 
591
                        variant = node.variant(env)
 
592
                        try:
 
593
                                time = bld.node_sigs[variant][node.id]
 
594
                        except KeyError:
 
595
                                debug("task: task %r must run as the first node does not exist" % self)
 
596
                                time = None
 
597
                                break
 
598
 
 
599
                # if one of the nodes does not exist, try to retrieve them from the cache
 
600
                if time is None and self.outputs:
 
601
                        try:
 
602
                                new_sig = self.signature()
 
603
                        except KeyError:
 
604
                                debug("task: something is wrong, computing the task signature failed")
 
605
                                return RUN_ME
 
606
 
 
607
                        return RUN_ME
 
608
 
 
609
                key = self.unique_id()
252
610
                try:
253
 
                        node = self.m_outputs[0]
254
 
                        variant = node.variant(self.m_env)
255
 
                        time = tree.m_tstamp_variants[variant][node]
256
 
                        key = hash( (variant, node, time, self.m_scanner.__class__.__name__) )
257
 
                        prev_sig = tree.m_sig_cache[key][0]
 
611
                        prev_sig = bld.task_sigs[key][0]
258
612
                except KeyError:
259
 
                        # an exception here means the object files do not exist
260
 
                        debug("task #%d should run as the first node does not exist" % self.m_idx, 'task')
261
 
 
262
 
                        # maybe we can just retrieve the object files from the cache then
263
 
                        ret = self.can_retrieve_cache(self.signature())
264
 
                        return not ret
265
 
 
 
613
                        debug("task: task %r must run as it was never run before or the task code changed" % self)
 
614
                        return RUN_ME
 
615
 
 
616
                #print "prev_sig is ", prev_sig
266
617
                new_sig = self.signature()
267
618
 
268
619
                # debug if asked to
269
 
                if Params.g_zones:
270
 
                        self.debug_why(tree.m_sig_cache[key])
271
 
 
 
620
                if Logs.verbose: self.debug_why(bld.task_sigs[key])
272
621
 
273
622
                if new_sig != prev_sig:
274
 
                        # if the node has not changed, try to use the cache
275
 
                        ret = self.can_retrieve_cache(new_sig)
276
 
                        return not ret
277
 
 
278
 
                return 0
279
 
 
280
 
        def update_stat(self):
281
 
                "this is called after a sucessful task run"
282
 
                tree = Params.g_build
283
 
                env  = self.m_env
 
623
                        return RUN_ME
 
624
                return SKIP_ME
 
625
 
 
626
        def post_run(self):
 
627
                "called after a successful task run"
 
628
                bld = self.generator.bld
 
629
                env = self.env
284
630
                sig = self.signature()
285
631
 
286
632
                cnt = 0
287
 
                for node in self.m_outputs:
 
633
                for node in self.outputs:
288
634
                        variant = node.variant(env)
289
 
                        #if node in tree.m_tstamp_variants[variant]:
 
635
                        #if node in bld.node_sigs[variant]:
290
636
                        #       print "variant is ", variant
291
 
                        #       print "self sig is ", Params.vsig(tree.m_tstamp_variants[variant][node])
 
637
                        #       print "self sig is ", Utils.view_sig(bld.node_sigs[variant][node])
292
638
 
293
639
                        # check if the node exists ..
294
 
                        try:
295
 
                                os.stat(node.abspath(env))
296
 
                        except:
297
 
                                error('a node was not produced for task %s %s' % (str(self.m_idx), node.abspath(env)))
298
 
                                raise
 
640
                        os.stat(node.abspath(env))
299
641
 
300
642
                        # important, store the signature for the next run
301
 
                        tree.m_tstamp_variants[variant][node] = sig
 
643
                        bld.node_sigs[variant][node.id] = sig
302
644
 
303
645
                        # We could re-create the signature of the task with the signature of the outputs
304
646
                        # in practice, this means hashing the output files
305
647
                        # this is unnecessary
306
 
 
307
 
                        if Params.g_usecache:
 
648
                        if Options.cache_global:
308
649
                                ssig = sig.encode('hex')
309
 
                                dest = os.path.join(Params.g_usecache, ssig+'-'+str(cnt))
 
650
                                dest = os.path.join(Options.cache_global, ssig+'-'+str(cnt))
310
651
                                try: shutil.copy2(node.abspath(env), dest)
311
 
                                except IOError: warning('could not write the file to the cache')
 
652
                                except IOError: warn('Could not write the file to the cache')
312
653
                                cnt += 1
313
654
 
314
 
                # keep the signatures in the first node
315
 
                node = self.m_outputs[0]
316
 
                variant = node.variant(self.m_env)
317
 
                time = tree.m_tstamp_variants[variant][node]
318
 
                key = hash( (variant, node, time, self.m_scanner.__class__.__name__) )
319
 
                val = self.cache_sig
320
 
                tree.set_sig_cache(key, val)
321
 
 
322
 
                self.m_executed=1
323
 
 
324
 
        def can_retrieve_cache(self, sig):
 
655
                bld.task_sigs[self.unique_id()] = self.cache_sig
 
656
                self.executed=1
 
657
 
 
658
        def can_retrieve_cache(self):
325
659
                """Retrieve build nodes from the cache - the file time stamps are updated
326
 
                for cleaning the least used files from the cache dir - be careful when overriding"""
327
 
                if not Params.g_usecache: return None
328
 
                if Params.g_options.nocache: return None
 
660
                for cleaning the least used files from the cache dir - be careful when overridding"""
 
661
                if not Options.cache_global: return None
 
662
                if Options.options.nocache: return None
329
663
 
330
 
                env  = self.m_env
 
664
                env = self.env
331
665
                sig = self.signature()
332
666
 
333
 
                try:
334
 
                        cnt = 0
335
 
                        for node in self.m_outputs:
336
 
                                variant = node.variant(env)
 
667
                cnt = 0
 
668
                for node in self.outputs:
 
669
                        variant = node.variant(env)
337
670
 
338
 
                                ssig = sig.encode('hex')
339
 
                                orig = os.path.join(Params.g_usecache, ssig+'-'+str(cnt))
 
671
                        ssig = sig.encode('hex')
 
672
                        orig = os.path.join(Options.cache_global, ssig+'-'+str(cnt))
 
673
                        try:
340
674
                                shutil.copy2(orig, node.abspath(env))
341
 
 
342
 
                                # touch the file
343
 
                                # what i would like to do is to limit the max size of the cache, using either
344
 
                                # df (file system full) or a fixed size (like say no more than 400Mb of cache)
345
 
                                # removing the files would be done by order of timestamps (TODO ITA)
 
675
                                # mark the cache file as used recently (modified)
346
676
                                os.utime(orig, None)
 
677
                        except (OSError, IOError):
 
678
                                debug('task: failed retrieving file')
 
679
                                return None
 
680
                        else:
347
681
                                cnt += 1
348
 
 
349
 
                                Params.g_build.m_tstamp_variants[variant][node] = sig
350
 
                                if not Runner.g_quiet: Params.pprint('GREEN', 'restored from cache %s' % node.bldpath(env))
351
 
                except:
352
 
                        debug("failed retrieving file", 'task')
353
 
                        return None
 
682
                                self.generator.bld.node_sigs[variant][node.id] = sig
 
683
                                self.generator.bld.printout('restoring from cache %r\n' % node.bldpath(env))
354
684
                return 1
355
685
 
356
 
        def prepare(self):
357
 
                try: self.m_action.prepare(self)
358
 
                except AttributeError: pass
359
 
 
360
 
        def run(self):
361
 
                return self.m_action.run(self)
362
 
 
363
 
        def get_display(self):
364
 
                if self.m_display: return self.m_display
365
 
                self.m_display=self.m_action.get_str(self)
366
 
                return self.m_display
367
 
 
368
 
        def color(self):
369
 
                return self.m_action.m_color
370
 
 
371
 
        def debug_info(self):
372
 
                ret = []
373
 
                ret.append('-- task details begin --')
374
 
                ret.append('action: %s' % str(self.m_action))
375
 
                ret.append('idx:    %s' % str(self.m_idx))
376
 
                ret.append('source: %s' % str(self.m_inputs))
377
 
                ret.append('target: %s' % str(self.m_outputs))
378
 
                ret.append('-- task details end --')
379
 
                return '\n'.join(ret)
380
 
 
381
 
        def debug(self, level=0):
382
 
                fun=Params.debug
383
 
                if level>0: fun=Params.error
384
 
                fun(self.debug_info())
385
 
 
386
686
        def debug_why(self, old_sigs):
387
687
                "explains why a task is run"
388
688
 
389
689
                new_sigs = self.cache_sig
390
 
                v = Params.vsig
391
 
 
392
 
                debug("Task %s must run: %s" % (self.m_idx, old_sigs[0] != new_sigs[0]), 'task')
393
 
                if (new_sigs[1] != old_sigs[1]):
394
 
                        debug(' -> A source file (or a dependency) has changed %s %s' % (v(old_sigs[1]), v(new_sigs[1])), 'task')
395
 
                if (new_sigs[2] != old_sigs[2]):
396
 
                        debug(' -> An environment variable has changed %s %s' % (v(old_sigs[2]), v(new_sigs[2])), 'task')
397
 
                if (new_sigs[3] != old_sigs[3]):
398
 
                        debug(' -> A manual dependency has changed %s %s' % (v(old_sigs[3]), v(new_sigs[3])), 'task')
399
 
                if (new_sigs[4] != old_sigs[4]):
400
 
                        debug(' -> A user-given environment variable has changed %s %s' % (v(old_sigs[4]), v(new_sigs[4])), 'task')
401
 
 
402
 
class TaskCmd(TaskBase):
403
 
        "TaskCmd executes commands. Instances always execute their function."
404
 
        def __init__(self, fun, env, priority):
405
 
                TaskBase.__init__(self, priority)
406
 
                self.fun = fun
407
 
                self.env = env
408
 
        def prepare(self):
409
 
                self.display = "* executing: "+self.fun.__name__
410
 
        def debug_info(self):
411
 
                return 'TaskCmd:fun %s' % self.fun.__name__
412
 
        def debug(self):
413
 
                return 'TaskCmd:fun %s' % self.fun.__name__
414
 
        def run(self):
415
 
                self.fun(self)
 
690
                def v(x):
 
691
                        return x.encode('hex')
 
692
 
 
693
                debug("Task %r" % self)
 
694
                msgs = ['Task must run', '* Source file or manual dependency', '* Implicit dependency', '* Environment variable']
 
695
                tmp = 'task: -> %s: %s %s'
 
696
                for x in xrange(len(msgs)):
 
697
                        if (new_sigs[x] != old_sigs[x]):
 
698
                                debug(tmp % (msgs[x], v(old_sigs[x]), v(new_sigs[x])))
 
699
 
 
700
        def sig_explicit_deps(self):
 
701
                bld = self.generator.bld
 
702
                m = md5()
 
703
 
 
704
                # the inputs
 
705
                for x in self.inputs:
 
706
                        variant = x.variant(self.env)
 
707
                        m.update(bld.node_sigs[variant][x.id])
 
708
 
 
709
                # additional nodes to depend on, if provided
 
710
                for x in getattr(self, 'dep_nodes', []):
 
711
                        variant = x.variant(self.env)
 
712
                        v = bld.node_sigs[variant][x.id]
 
713
                        m.update(v)
 
714
 
 
715
                # manual dependencies, they can slow down the builds
 
716
                try:
 
717
                        additional_deps = bld.deps_man
 
718
                except AttributeError:
 
719
                        pass
 
720
                else:
 
721
                        for x in self.inputs + self.outputs:
 
722
                                try:
 
723
                                        d = additional_deps[x.id]
 
724
                                except KeyError:
 
725
                                        continue
 
726
                                if callable(d):
 
727
                                        d = d() # dependency is a function, call it
 
728
 
 
729
                                for v in d:
 
730
                                        if isinstance(v, Node.Node):
 
731
                                                bld.rescan(v.parent)
 
732
                                                variant = v.variant(self.env)
 
733
                                                try:
 
734
                                                        v = bld.node_sigs[variant][v.id]
 
735
                                                except KeyError: # make it fatal?
 
736
                                                        v = ''
 
737
                                        m.update(v)
 
738
                return m.digest()
 
739
 
 
740
        def sig_vars(self):
 
741
                m = md5()
 
742
                bld = self.generator.bld
 
743
                env = self.env
 
744
 
 
745
                # dependencies on the environment vars
 
746
                act_sig = bld.hash_env_vars(env, self.__class__.vars)
 
747
                m.update(act_sig)
 
748
 
 
749
                # additional variable dependencies, if provided
 
750
                dep_vars = getattr(self, 'dep_vars', None)
 
751
                if dep_vars:
 
752
                        m.update(bld.hash_env_vars(env, dep_vars))
 
753
 
 
754
                return m.digest()
 
755
 
 
756
        #def scan(self, node):
 
757
        #       """this method returns a tuple containing:
 
758
        #       * a list of nodes corresponding to real files
 
759
        #       * a list of names for files not found in path_lst
 
760
        #       the input parameters may have more parameters that the ones used below
 
761
        #       """
 
762
        #       return ((), ())
 
763
        scan = None
 
764
 
 
765
        # compute the signature, recompute it if there is no match in the cache
 
766
        def sig_implicit_deps(self):
 
767
                "the signature obtained may not be the one if the files have changed, we do it in two steps"
 
768
 
 
769
                bld = self.generator.bld
 
770
 
 
771
                # get the task signatures from previous runs
 
772
                key = self.unique_id()
 
773
                prev_sigs = bld.task_sigs.get(key, ())
 
774
                if prev_sigs:
 
775
                        try:
 
776
                                if prev_sigs[2] == self.compute_sig_implicit_deps():
 
777
                                        return prev_sigs[2]
 
778
                        except KeyError:
 
779
                                pass
 
780
 
 
781
                # no previous run or the signature of the dependencies has changed, rescan the dependencies
 
782
                (nodes, names) = self.scan()
 
783
                if Logs.verbose:
 
784
                        debug('deps: scanner for %s returned %s %s' % (str(self), str(nodes), str(names)))
 
785
 
 
786
                # store the dependencies in the cache
 
787
                bld.node_deps[key] = nodes
 
788
                bld.raw_deps[key] = names
 
789
 
 
790
                # recompute the signature and return it
 
791
                sig = self.compute_sig_implicit_deps()
 
792
 
 
793
                return sig
 
794
 
 
795
        def compute_sig_implicit_deps(self):
 
796
                """it is intented for .cpp and inferred .h files
 
797
                there is a single list (no tree traversal)
 
798
                this is the hot spot so ... do not touch"""
 
799
                m = md5()
 
800
                upd = m.update
 
801
 
 
802
                bld = self.generator.bld
 
803
                tstamp = bld.node_sigs
 
804
                env = self.env
 
805
 
 
806
                for k in bld.node_deps.get(self.unique_id(), []) + self.inputs:
 
807
                        # unlikely but necessary if it happens
 
808
                        if not k.parent.id in bld.cache_scanned_folders:
 
809
                                bld.rescan(k.parent)
 
810
 
 
811
                        if k.id & 3 == Node.FILE:
 
812
                                upd(tstamp[0][k.id])
 
813
                        else:
 
814
                                upd(tstamp[env.variant()][k.id])
 
815
 
 
816
                return m.digest()
 
817
 
 
818
def funex(c):
 
819
        dc = {}
 
820
        exec(c, dc)
 
821
        return dc['f']
 
822
 
 
823
reg_act = re.compile(r"(?P<dollar>\$\$)|(?P<subst>\$\{(?P<var>\w+)(?P<code>.*?)\})", re.M)
 
824
def compile_fun(name, line):
 
825
        """Compiles a string (once) into a function, eg:
 
826
        simple_task_type('c++', '${CXX} -o ${TGT[0]} ${SRC} -I ${SRC[0].parent.bldpath()}')
 
827
 
 
828
        The env variables (CXX, ..) on the task must not hold dicts (order)
 
829
        The reserved keywords TGT and SRC represent the task input and output nodes
 
830
        """
 
831
        extr = []
 
832
        def repl(match):
 
833
                g = match.group
 
834
                if g('dollar'): return "$"
 
835
                elif g('subst'): extr.append((g('var'), g('code'))); return "%s"
 
836
                return None
 
837
 
 
838
        line = reg_act.sub(repl, line)
 
839
 
 
840
        parm = []
 
841
        dvars = []
 
842
        app = parm.append
 
843
        for (var, meth) in extr:
 
844
                if var == 'SRC':
 
845
                        if meth: app('task.inputs%s' % meth)
 
846
                        else: app('" ".join([a.srcpath(env) for a in task.inputs])')
 
847
                elif var == 'TGT':
 
848
                        if meth: app('task.outputs%s' % meth)
 
849
                        else: app('" ".join([a.bldpath(env) for a in task.outputs])')
 
850
                else:
 
851
                        if not var in dvars: dvars.append(var)
 
852
                        app("p('%s')" % var)
 
853
        if parm: parm = "%% (%s) " % (',\n\t\t'.join(parm))
 
854
        else: parm = ''
 
855
 
 
856
        c = '''
 
857
def f(task):
 
858
        env = task.env
 
859
        wd = getattr(task, 'cwd', None)
 
860
        p = env.get_flat
 
861
        cmd = "%s" % s
 
862
        return task.generator.bld.exec_command(cmd, cwd=wd)
 
863
''' % (line, parm)
 
864
 
 
865
        debug('action: %s' % c)
 
866
        return (funex(c), dvars)
 
867
 
 
868
def simple_task_type(name, line, color='GREEN', vars=[], ext_in=[], ext_out=[], before=[], after=[]):
 
869
        """return a new Task subclass with the function run compiled from the line given"""
 
870
        (fun, dvars) = compile_fun(name, line)
 
871
        fun.code = line
 
872
        return task_type_from_func(name, fun, vars or dvars, color, ext_in, ext_out, before, after)
 
873
 
 
874
def task_type_from_func(name, func, vars=[], color='GREEN', ext_in=[], ext_out=[], before=[], after=[]):
 
875
        """return a new Task subclass with the function run compiled from the line given"""
 
876
        params = {
 
877
                'run': func,
 
878
                'vars': vars,
 
879
                'color': color,
 
880
                'name': name,
 
881
                'ext_in': Utils.to_list(ext_in),
 
882
                'ext_out': Utils.to_list(ext_out),
 
883
                'before': Utils.to_list(before),
 
884
                'after': Utils.to_list(after),
 
885
        }
 
886
 
 
887
        cls = type(Task)(name, (Task,), params)
 
888
        TaskBase.classes[name] = cls
 
889
        return cls
 
890
 
 
891
def always_run(cls):
 
892
        """Set all task instances of this class to be executed whenever a build is started
 
893
        The task signature is calculated, but the result of the comparation between
 
894
        task signatures is bypassed
 
895
        """
 
896
        old = cls.runnable_status
 
897
        def always(self):
 
898
                old(self)
 
899
                return RUN_ME
 
900
        cls.runnable_status = always
 
901
 
 
902
def update_outputs(cls):
 
903
        """When a command is always run, it is possible that the output only change
 
904
        sometimes. By default the build node have as a hash the signature of the task
 
905
        which may not change. With this, the output nodes (produced) are hashed,
 
906
        and the hashes are set to the build nodes
 
907
 
 
908
        This may avoid unnecessary recompilations, but it uses more resources
 
909
        (hashing the output files) so it is not used by default
 
910
        """
 
911
        old_post_run = cls.post_run
 
912
        def post_run(self):
 
913
                old_post_run(self)
 
914
                bld = self.outputs[0].__class__.bld
 
915
                bld.node_sigs[self.env.variant()][self.outputs[0].id] = \
 
916
                Utils.h_file(self.outputs[0].abspath(self.env))
 
917
        cls.post_run = post_run
416
918