2
0
mirror of https://gitlab.com/ita1024/waf.git synced 2024-11-29 21:41:44 +01:00
waf/waflib/Task.py

1282 lines
35 KiB
Python
Raw Normal View History

2011-09-10 11:13:51 +02:00
#!/usr/bin/env python
# encoding: utf-8
2018-01-01 20:53:49 +01:00
# Thomas Nagy, 2005-2018 (ita)
2011-09-10 11:13:51 +02:00
"""
Tasks represent atomic operations such as processes.
"""
import os, re, sys, tempfile, traceback
2011-09-10 11:13:51 +02:00
from waflib import Utils, Logs, Errors
# task states
NOT_RUN = 0
"""The task was not executed yet"""
MISSING = 1
"""The task has been executed but the files have not been created"""
CRASHED = 2
"""The task execution returned a non-zero exit status"""
EXCEPTION = 3
2017-02-16 07:03:43 +01:00
"""An exception occurred in the task execution"""
2011-09-10 11:13:51 +02:00
CANCELED = 4
"""A dependency for the task is missing so it was cancelled"""
2011-09-10 11:13:51 +02:00
SKIPPED = 8
"""The task did not have to be executed"""
SUCCESS = 9
"""The task was successfully executed"""
ASK_LATER = -1
"""The task is not ready to be executed"""
SKIP_ME = -2
"""The task does not need to be executed"""
RUN_ME = -3
"""The task must be executed"""
CANCEL_ME = -4
"""The task cannot be executed because of a dependency problem"""
2011-09-10 11:13:51 +02:00
COMPILE_TEMPLATE_SHELL = '''
def f(tsk):
env = tsk.env
gen = tsk.generator
bld = gen.bld
2016-01-11 05:25:46 +01:00
cwdx = tsk.get_cwd()
2011-09-10 11:13:51 +02:00
p = env.get_flat
tsk.last_cmd = cmd = \'\'\' %s \'\'\' % s
2015-12-24 23:10:56 +01:00
return tsk.exec_command(cmd, cwd=cwdx, env=env.env or None)
'''
2011-09-10 11:13:51 +02:00
COMPILE_TEMPLATE_NOSHELL = '''
def f(tsk):
env = tsk.env
gen = tsk.generator
bld = gen.bld
2016-01-11 05:25:46 +01:00
cwdx = tsk.get_cwd()
2011-09-10 11:13:51 +02:00
def to_list(xx):
if isinstance(xx, str): return [xx]
return xx
def merge(lst1, lst2):
if lst1 and lst2:
return lst1[:-1] + [lst1[-1] + lst2[0]] + lst2[1:]
return lst1 + lst2
lst = []
2011-09-10 11:13:51 +02:00
%s
if '' in lst:
lst = [x for x in lst if x]
tsk.last_cmd = lst
2015-12-24 23:10:56 +01:00
return tsk.exec_command(lst, cwd=cwdx, env=env.env or None)
'''
2011-09-10 11:13:51 +02:00
classes = {}
2016-06-25 16:23:06 +02:00
"""
The metaclass :py:class:`waflib.Task.store_task_type` stores all class tasks
created by user scripts or Waf tools to this dict. It maps class names to class objects.
"""
2011-09-10 11:13:51 +02:00
class store_task_type(type):
"""
2016-06-25 16:23:06 +02:00
Metaclass: store the task classes into the dict pointed by the
class attribute 'register' which defaults to :py:const:`waflib.Task.classes`,
The attribute 'run_str' is compiled into a method 'run' bound to the task class.
2011-09-10 11:13:51 +02:00
"""
def __init__(cls, name, bases, dict):
super(store_task_type, cls).__init__(name, bases, dict)
name = cls.__name__
if name != 'evil' and name != 'Task':
2011-09-10 11:13:51 +02:00
if getattr(cls, 'run_str', None):
# if a string is provided, convert it to a method
(f, dvars) = compile_fun(cls.run_str, cls.shell)
cls.hcode = Utils.h_cmd(cls.run_str)
cls.orig_run_str = cls.run_str
# change the name of run_str or it is impossible to subclass with a function
2011-09-10 11:13:51 +02:00
cls.run_str = None
cls.run = f
cls.vars = list(set(cls.vars + dvars))
cls.vars.sort()
elif getattr(cls, 'run', None) and not 'hcode' in cls.__dict__:
# getattr(cls, 'hcode') would look in the upper classes
cls.hcode = Utils.h_cmd(cls.run)
# be creative
getattr(cls, 'register', classes)[name] = cls
2011-09-10 11:13:51 +02:00
evil = store_task_type('evil', (object,), {})
"Base class provided to avoid writing a metaclass, so the code can run in python 2.6 and 3.x unmodified"
class Task(evil):
2011-09-10 11:13:51 +02:00
"""
This class deals with the filesystem (:py:class:`waflib.Node.Node`). The method :py:class:`waflib.Task.Task.runnable_status`
uses a hash value (from :py:class:`waflib.Task.Task.signature`) which is persistent from build to build. When the value changes,
the task has to be executed. The method :py:class:`waflib.Task.Task.post_run` will assign the task signature to the output
nodes (if present).
"""
vars = []
"""ConfigSet variables that should trigger a rebuild (class attribute used for :py:meth:`waflib.Task.Task.sig_vars`)"""
2011-09-10 11:13:51 +02:00
always_run = False
"""Specify whether task instances must always be executed or not (class attribute)"""
2011-09-10 11:13:51 +02:00
shell = False
"""Execute the command with the shell (class attribute)"""
2011-09-10 11:13:51 +02:00
color = 'GREEN'
"""Color for the console display, see :py:const:`waflib.Logs.colors_lst`"""
ext_in = []
2016-06-25 16:23:06 +02:00
"""File extensions that objects of this task class may use"""
2011-09-10 11:13:51 +02:00
ext_out = []
2016-06-25 16:23:06 +02:00
"""File extensions that objects of this task class may create"""
2011-09-10 11:13:51 +02:00
before = []
"""List of task class names to execute before instances of this class"""
after = []
"""List of task class names to execute after instances of this class"""
2017-02-12 15:42:40 +01:00
hcode = Utils.SIG_NIL
2011-09-10 11:13:51 +02:00
"""String representing an additional hash for the class representation"""
keep_last_cmd = False
"""Whether to keep the last command executed on the instance after execution.
This may be useful for certain extensions but it can a lot of memory.
"""
weight = 0
"""Optional weight to tune the priority for task instances.
The higher, the earlier. The weight only applies to single task objects."""
tree_weight = 0
"""Optional weight to tune the priority of task instances and whole subtrees.
The higher, the earlier."""
prio_order = 0
"""Priority order set by the scheduler on instances during the build phase.
You most likely do not need to set it.
"""
__slots__ = ('hasrun', 'generator', 'env', 'inputs', 'outputs', 'dep_nodes', 'run_after')
2011-09-10 11:13:51 +02:00
def __init__(self, *k, **kw):
self.hasrun = NOT_RUN
try:
self.generator = kw['generator']
except KeyError:
self.generator = self
self.env = kw['env']
""":py:class:`waflib.ConfigSet.ConfigSet` object (make sure to provide one)"""
2011-09-10 11:13:51 +02:00
self.inputs = []
"""List of input nodes, which represent the files used by the task instance"""
2011-09-10 11:13:51 +02:00
self.outputs = []
"""List of output nodes, which represent the files created by the task instance"""
self.dep_nodes = []
"""List of additional nodes to depend on"""
self.run_after = set()
"""Set of tasks that must be executed before this one"""
2017-06-14 18:59:28 +02:00
def __lt__(self, other):
return self.priority() > other.priority()
2017-06-14 18:59:28 +02:00
def __le__(self, other):
return self.priority() >= other.priority()
2017-06-14 18:59:28 +02:00
def __gt__(self, other):
return self.priority() < other.priority()
2017-06-14 18:59:28 +02:00
def __ge__(self, other):
return self.priority() <= other.priority()
2017-06-14 18:59:28 +02:00
2016-01-11 05:25:46 +01:00
def get_cwd(self):
2016-06-25 16:23:06 +02:00
"""
:return: current working directory
:rtype: :py:class:`waflib.Node.Node`
"""
2016-01-11 05:25:46 +01:00
bld = self.generator.bld
2016-09-10 10:44:08 +02:00
ret = getattr(self, 'cwd', None) or getattr(bld, 'cwd', bld.bldnode)
2016-01-11 05:25:46 +01:00
if isinstance(ret, str):
2016-09-10 10:44:08 +02:00
if os.path.isabs(ret):
ret = bld.root.make_node(ret)
else:
ret = self.generator.path.make_node(ret)
2016-01-11 05:25:46 +01:00
return ret
2016-06-15 20:24:34 +02:00
def quote_flag(self, x):
2016-06-25 16:23:06 +02:00
"""
Surround a process argument by quotes so that a list of arguments can be written to a file
:param x: flag
:type x: string
:return: quoted flag
:rtype: string
"""
2016-06-15 20:24:34 +02:00
old = x
if '\\' in x:
x = x.replace('\\', '\\\\')
if '"' in x:
x = x.replace('"', '\\"')
if old != x or ' ' in x or '\t' in x or "'" in x:
x = '"%s"' % x
return x
2017-02-12 15:04:48 +01:00
def priority(self):
"""
Priority of execution; the higher, the earlier
2017-02-12 15:04:48 +01:00
:return: the priority value
:rtype: a tuple of numeric values
2017-02-12 15:04:48 +01:00
"""
return (self.weight + self.prio_order, - getattr(self.generator, 'tg_idx_count', 0))
2017-02-12 15:04:48 +01:00
2016-06-15 20:24:34 +02:00
def split_argfile(self, cmd):
2016-06-25 16:23:06 +02:00
"""
Splits a list of process commands into the executable part and its list of arguments
:return: a tuple containing the executable first and then the rest of arguments
:rtype: tuple
"""
2016-06-15 20:24:34 +02:00
return ([cmd[0]], [self.quote_flag(x) for x in cmd[1:]])
2011-09-10 11:13:51 +02:00
def exec_command(self, cmd, **kw):
"""
2016-06-25 16:23:06 +02:00
Wrapper for :py:meth:`waflib.Context.Context.exec_command`.
This version set the current working directory (``build.variant_dir``),
applies PATH settings (if self.env.PATH is provided), and can run long
commands through a temporary ``@argfile``.
2011-09-10 11:13:51 +02:00
2016-06-25 16:23:06 +02:00
:param cmd: process command to execute
:type cmd: list of string (best) or string (process will use a shell)
2011-09-10 11:13:51 +02:00
:return: the return code
:rtype: int
Optional parameters:
#. cwd: current working directory (Node or string)
#. stdout: set to None to prevent waf from capturing the process standard output
#. stderr: set to None to prevent waf from capturing the process standard error
#. timeout: timeout value (Python 3)
2011-09-10 11:13:51 +02:00
"""
2015-12-24 23:10:56 +01:00
if not 'cwd' in kw:
2016-01-11 05:25:46 +01:00
kw['cwd'] = self.get_cwd()
2016-06-15 20:24:34 +02:00
if hasattr(self, 'timeout'):
kw['timeout'] = self.timeout
if self.env.PATH:
env = kw['env'] = dict(kw.get('env') or self.env.env or os.environ)
env['PATH'] = self.env.PATH if isinstance(self.env.PATH, str) else os.pathsep.join(self.env.PATH)
if hasattr(self, 'stdout'):
kw['stdout'] = self.stdout
if hasattr(self, 'stderr'):
kw['stderr'] = self.stderr
2016-06-15 20:24:34 +02:00
# workaround for command line length limit:
# http://support.microsoft.com/kb/830473
2016-06-15 22:24:30 +02:00
if not isinstance(cmd, str) and (len(repr(cmd)) >= 8192 if Utils.is_win32 else len(cmd) > 200000):
2016-06-15 20:24:34 +02:00
cmd, args = self.split_argfile(cmd)
try:
(fd, tmp) = tempfile.mkstemp()
os.write(fd, '\r\n'.join(args).encode())
os.close(fd)
if Logs.verbose:
Logs.debug('argfile: @%r -> %r', tmp, args)
2016-06-15 20:24:34 +02:00
return self.generator.bld.exec_command(cmd + ['@' + tmp], **kw)
finally:
try:
os.remove(tmp)
except OSError:
# anti-virus and indexers can keep files open -_-
pass
else:
return self.generator.bld.exec_command(cmd, **kw)
2011-09-10 11:13:51 +02:00
def process(self):
"""
2017-08-27 10:11:56 +02:00
Runs the task and handles errors
2016-06-25 16:23:06 +02:00
:return: 0 or None if everything is fine
:rtype: integer
2011-09-10 11:13:51 +02:00
"""
# remove the task signature immediately before it is executed
2017-08-27 10:11:56 +02:00
# so that the task will be executed again in case of failure
2011-09-10 11:13:51 +02:00
try:
del self.generator.bld.task_sigs[self.uid()]
2012-02-11 14:43:07 +01:00
except KeyError:
2011-09-10 11:13:51 +02:00
pass
try:
ret = self.run()
except Exception:
self.err_msg = traceback.format_exc()
2011-09-10 11:13:51 +02:00
self.hasrun = EXCEPTION
else:
2017-08-27 10:11:56 +02:00
if ret:
self.err_code = ret
self.hasrun = CRASHED
else:
try:
self.post_run()
except Errors.WafError:
pass
except Exception:
self.err_msg = traceback.format_exc()
self.hasrun = EXCEPTION
else:
self.hasrun = SUCCESS
if self.hasrun != SUCCESS and self.scan:
# rescan dependencies on next run
2011-09-10 11:13:51 +02:00
try:
2017-08-27 10:11:56 +02:00
del self.generator.bld.imp_sigs[self.uid()]
except KeyError:
2011-09-10 11:13:51 +02:00
pass
def log_display(self, bld):
2016-06-25 16:23:06 +02:00
"Writes the execution status on the context logger"
if self.generator.bld.progress_bar == 3:
return
s = self.display()
if s:
if bld.logger:
logger = bld.logger
else:
logger = Logs
if self.generator.bld.progress_bar == 1:
c1 = Logs.colors.cursor_off
c2 = Logs.colors.cursor_on
logger.info(s, extra={'stream': sys.stderr, 'terminator':'', 'c1': c1, 'c2' : c2})
else:
logger.info(s, extra={'terminator':'', 'c1': '', 'c2' : ''})
2011-09-10 11:13:51 +02:00
def display(self):
"""
2016-06-25 16:23:06 +02:00
Returns an execution status for the console, the progress bar, or the IDE output.
2011-09-10 11:13:51 +02:00
:rtype: string
"""
col1 = Logs.colors(self.color)
col2 = Logs.colors.NORMAL
2016-02-14 21:12:37 +01:00
master = self.generator.bld.producer
2011-09-10 11:13:51 +02:00
def cur():
# the current task position, computed as late as possible
return master.processed - master.ready.qsize()
2011-09-10 11:13:51 +02:00
if self.generator.bld.progress_bar == 1:
return self.generator.bld.progress_line(cur(), master.total, col1, col2)
if self.generator.bld.progress_bar == 2:
ela = str(self.generator.bld.timer)
try:
ins = ','.join([n.name for n in self.inputs])
except AttributeError:
ins = ''
try:
outs = ','.join([n.name for n in self.outputs])
except AttributeError:
outs = ''
return '|Total %s|Current %s|Inputs %s|Outputs %s|Time %s|\n' % (master.total, cur(), ins, outs, ela)
s = str(self)
if not s:
return None
total = master.total
n = len(str(total))
fs = '[%%%dd/%%%dd] %%s%%s%%s%%s\n' % (n, n)
kw = self.keyword()
if kw:
kw += ' '
return fs % (cur(), total, kw, col1, s, col2)
2011-09-10 11:13:51 +02:00
def hash_constraints(self):
"""
2016-06-25 16:23:06 +02:00
Identifies a task type for all the constraints relevant for the scheduler: precedence, file production
2011-09-10 11:13:51 +02:00
:return: a hash value
:rtype: string
"""
return (tuple(self.before), tuple(self.after), tuple(self.ext_in), tuple(self.ext_out), self.__class__.__name__, self.hcode)
2011-09-10 11:13:51 +02:00
def format_error(self):
"""
2016-06-25 16:23:06 +02:00
Returns an error message to display the build failure reasons
2011-09-10 11:13:51 +02:00
:rtype: string
"""
if Logs.verbose:
msg = ': %r\n%r' % (self, getattr(self, 'last_cmd', ''))
else:
msg = ' (run with -v to display more information)'
2012-01-25 09:36:33 +01:00
name = getattr(self.generator, 'name', '')
2011-09-10 11:13:51 +02:00
if getattr(self, "err_msg", None):
return self.err_msg
2011-10-17 18:37:19 +02:00
elif not self.hasrun:
2012-01-25 09:36:33 +01:00
return 'task in %r was not executed for some reason: %r' % (name, self)
2011-09-10 11:13:51 +02:00
elif self.hasrun == CRASHED:
try:
return ' -> task in %r failed with exit status %r%s' % (name, self.err_code, msg)
2011-09-10 11:13:51 +02:00
except AttributeError:
return ' -> task in %r failed%s' % (name, msg)
2011-09-10 11:13:51 +02:00
elif self.hasrun == MISSING:
return ' -> missing files in %r%s' % (name, msg)
elif self.hasrun == CANCELED:
return ' -> %r canceled because of missing dependencies' % name
2011-09-10 11:13:51 +02:00
else:
2012-01-25 09:36:33 +01:00
return 'invalid status for task in %r: %r' % (name, self.hasrun)
2011-09-10 11:13:51 +02:00
def colon(self, var1, var2):
"""
2016-06-25 16:23:06 +02:00
Enable scriptlet expressions of the form ${FOO_ST:FOO}
2013-08-17 20:24:21 +02:00
If the first variable (FOO_ST) is empty, then an empty list is returned
2011-09-10 11:13:51 +02:00
2013-08-17 20:24:21 +02:00
The results will be slightly different if FOO_ST is a list, for example::
env.FOO = ['p1', 'p2']
2013-08-17 20:24:21 +02:00
env.FOO_ST = '-I%s'
# ${FOO_ST:FOO} returns
['-Ip1', '-Ip2']
env.FOO_ST = ['-a', '-b']
2013-08-17 20:24:21 +02:00
# ${FOO_ST:FOO} returns
['-a', '-b', 'p1', '-a', '-b', 'p2']
2011-09-10 11:13:51 +02:00
"""
tmp = self.env[var1]
2013-08-17 20:24:21 +02:00
if not tmp:
return []
2011-09-10 11:13:51 +02:00
if isinstance(var2, str):
it = self.env[var2]
else:
it = var2
if isinstance(tmp, str):
return [tmp % x for x in it]
else:
lst = []
for y in it:
lst.extend(tmp)
lst.append(y)
return lst
def __str__(self):
"string to display to the user"
name = self.__class__.__name__
if self.outputs:
if name.endswith(('lib', 'program')) or not self.inputs:
node = self.outputs[0]
return node.path_from(node.ctx.launch_node())
if not (self.inputs or self.outputs):
return self.__class__.__name__
if len(self.inputs) == 1:
node = self.inputs[0]
return node.path_from(node.ctx.launch_node())
2014-05-19 21:32:24 +02:00
src_str = ' '.join([a.path_from(a.ctx.launch_node()) for a in self.inputs])
tgt_str = ' '.join([a.path_from(a.ctx.launch_node()) for a in self.outputs])
if self.outputs:
sep = ' -> '
else:
sep = ''
return '%s: %s%s%s' % (self.__class__.__name__, src_str, sep, tgt_str)
def keyword(self):
"Display keyword used to prettify the console outputs"
name = self.__class__.__name__
if name.endswith(('lib', 'program')):
return 'Linking'
if len(self.inputs) == 1 and len(self.outputs) == 1:
return 'Compiling'
if not self.inputs:
if self.outputs:
return 'Creating'
else:
return 'Running'
return 'Processing'
2011-09-10 11:13:51 +02:00
def __repr__(self):
"for debugging purposes"
2012-06-20 19:10:34 +02:00
try:
ins = ",".join([x.name for x in self.inputs])
outs = ",".join([x.name for x in self.outputs])
except AttributeError:
2012-06-20 19:10:34 +02:00
ins = ",".join([str(x) for x in self.inputs])
outs = ",".join([str(x) for x in self.outputs])
return "".join(['\n\t{task %r: ' % id(self), self.__class__.__name__, " ", ins, " -> ", outs, '}'])
2011-09-10 11:13:51 +02:00
def uid(self):
"""
2016-06-25 16:23:06 +02:00
Returns an identifier used to determine if tasks are up-to-date. Since the
2011-09-10 11:13:51 +02:00
identifier will be stored between executions, it must be:
2016-06-25 16:23:06 +02:00
- unique for a task: no two tasks return the same value (for a given build context)
2011-09-10 11:13:51 +02:00
- the same for a given task instance
By default, the node paths, the class name, and the function are used
as inputs to compute a hash.
The pointer to the object (python built-in 'id') will change between build executions,
and must be avoided in such hashes.
:return: hash value
:rtype: string
"""
try:
return self.uid_
except AttributeError:
2016-03-05 11:05:43 +01:00
m = Utils.md5(self.__class__.__name__)
2011-09-10 11:13:51 +02:00
up = m.update
for x in self.inputs + self.outputs:
up(x.abspath())
2011-09-10 11:13:51 +02:00
self.uid_ = m.digest()
return self.uid_
def set_inputs(self, inp):
"""
2016-06-25 16:23:06 +02:00
Appends the nodes to the *inputs* list
2011-09-10 11:13:51 +02:00
:param inp: input nodes
:type inp: node or list of nodes
"""
if isinstance(inp, list):
self.inputs += inp
else:
self.inputs.append(inp)
2011-09-10 11:13:51 +02:00
def set_outputs(self, out):
"""
2016-06-25 16:23:06 +02:00
Appends the nodes to the *outputs* list
2011-09-10 11:13:51 +02:00
:param out: output nodes
:type out: node or list of nodes
"""
if isinstance(out, list):
self.outputs += out
else:
self.outputs.append(out)
2011-09-10 11:13:51 +02:00
def set_run_after(self, task):
"""
2016-06-25 16:23:06 +02:00
Run this task only after the given *task*.
2011-09-10 11:13:51 +02:00
:param task: task
:type task: :py:class:`waflib.Task.Task`
"""
assert isinstance(task, Task)
2011-09-10 11:13:51 +02:00
self.run_after.add(task)
def signature(self):
"""
Task signatures are stored between build executions, they are use to track the changes
made to the input nodes (not to the outputs!). The signature hashes data from various sources:
* explicit dependencies: files listed in the inputs (list of node objects) :py:meth:`waflib.Task.Task.sig_explicit_deps`
* implicit dependencies: list of nodes returned by scanner methods (when present) :py:meth:`waflib.Task.Task.sig_implicit_deps`
2016-12-07 19:04:46 +01:00
* hashed data: variables/values read from task.vars/task.env :py:meth:`waflib.Task.Task.sig_vars`
2011-09-10 11:13:51 +02:00
If the signature is expected to give a different result, clear the cache kept in ``self.cache_sig``::
from waflib import Task
class cls(Task.Task):
def signature(self):
sig = super(Task.Task, self).signature()
delattr(self, 'cache_sig')
return super(Task.Task, self).signature()
2016-06-25 16:23:06 +02:00
:return: the signature value
:rtype: string or bytes
2011-09-10 11:13:51 +02:00
"""
2016-06-25 16:23:06 +02:00
try:
return self.cache_sig
except AttributeError:
pass
2011-09-10 11:13:51 +02:00
2016-03-05 11:05:43 +01:00
self.m = Utils.md5(self.hcode)
2011-09-10 11:13:51 +02:00
# explicit deps
self.sig_explicit_deps()
# env vars
self.sig_vars()
# implicit deps / scanner results
if self.scan:
try:
self.sig_implicit_deps()
except Errors.TaskRescan:
return self.signature()
ret = self.cache_sig = self.m.digest()
return ret
def runnable_status(self):
"""
Returns the Task status
:return: a task state in :py:const:`waflib.Task.RUN_ME`,
:py:const:`waflib.Task.SKIP_ME`, :py:const:`waflib.Task.CANCEL_ME` or :py:const:`waflib.Task.ASK_LATER`.
:rtype: int
2011-09-10 11:13:51 +02:00
"""
2017-01-22 14:40:36 +01:00
bld = self.generator.bld
if bld.is_install < 0:
return SKIP_ME
2011-09-10 11:13:51 +02:00
for t in self.run_after:
if not t.hasrun:
return ASK_LATER
elif t.hasrun < SKIPPED:
# a dependency has an error
return CANCEL_ME
2011-09-10 11:13:51 +02:00
# first compute the signature
try:
new_sig = self.signature()
except Errors.TaskNotReady:
return ASK_LATER
# compare the signature to a signature computed previously
key = self.uid()
try:
prev_sig = bld.task_sigs[key]
except KeyError:
2016-06-05 00:23:57 +02:00
Logs.debug('task: task %r must run: it was never run before or the task code changed', self)
2015-12-23 17:50:48 +01:00
return RUN_ME
if new_sig != prev_sig:
2016-06-05 00:23:57 +02:00
Logs.debug('task: task %r must run: the task signature changed', self)
2011-09-10 11:13:51 +02:00
return RUN_ME
# compare the signatures of the outputs
for node in self.outputs:
2016-04-19 22:00:21 +02:00
sig = bld.node_sigs.get(node)
2015-12-23 17:50:48 +01:00
if not sig:
2016-06-05 00:23:57 +02:00
Logs.debug('task: task %r must run: an output node has no signature', self)
2015-12-23 17:50:48 +01:00
return RUN_ME
if sig != key:
2016-06-05 00:23:57 +02:00
Logs.debug('task: task %r must run: an output node was produced by another task', self)
2015-12-23 17:50:48 +01:00
return RUN_ME
if not node.exists():
2016-06-05 00:23:57 +02:00
Logs.debug('task: task %r must run: an output node does not exist', self)
2011-09-10 11:13:51 +02:00
return RUN_ME
return (self.always_run and RUN_ME) or SKIP_ME
2011-09-10 11:13:51 +02:00
def post_run(self):
"""
2016-06-25 16:23:06 +02:00
Called after successful execution to record that the task has run by
updating the entry in :py:attr:`waflib.Build.BuildContext.task_sigs`.
2011-09-10 11:13:51 +02:00
"""
bld = self.generator.bld
2011-09-10 11:13:51 +02:00
for node in self.outputs:
if not node.exists():
2011-09-10 11:13:51 +02:00
self.hasrun = MISSING
self.err_msg = '-> missing file: %r' % node.abspath()
raise Errors.WafError(self.err_msg)
bld.node_sigs[node] = self.uid() # make sure this task produced the files in question
bld.task_sigs[self.uid()] = self.signature()
if not self.keep_last_cmd:
try:
del self.last_cmd
except AttributeError:
pass
2011-09-10 11:13:51 +02:00
def sig_explicit_deps(self):
"""
2016-06-25 16:23:06 +02:00
Used by :py:meth:`waflib.Task.Task.signature`; it hashes :py:attr:`waflib.Task.Task.inputs`
2011-09-10 11:13:51 +02:00
and :py:attr:`waflib.Task.Task.dep_nodes` signatures.
"""
bld = self.generator.bld
upd = self.m.update
# the inputs
for x in self.inputs + self.dep_nodes:
upd(x.get_bld_sig())
2011-09-10 11:13:51 +02:00
# manual dependencies, they can slow down the builds
if bld.deps_man:
additional_deps = bld.deps_man
for x in self.inputs + self.outputs:
try:
d = additional_deps[x]
2011-09-10 11:13:51 +02:00
except KeyError:
continue
for v in d:
try:
2015-12-23 17:34:17 +01:00
v = v.get_bld_sig()
except AttributeError:
if hasattr(v, '__call__'):
v = v() # dependency is a function, call it
2011-09-10 11:13:51 +02:00
upd(v)
def sig_deep_inputs(self):
"""
Enable rebuilds on input files task signatures. Not used by default.
Example: hashes of output programs can be unchanged after being re-linked,
despite the libraries being different. This method can thus prevent stale unit test
results (waf_unit_test.py).
Hashing input file timestamps is another possibility for the implementation.
This may cause unnecessary rebuilds when input tasks are frequently executed.
Here is an implementation example::
lst = []
for node in self.inputs + self.dep_nodes:
st = os.stat(node.abspath())
lst.append(st.st_mtime)
lst.append(st.st_size)
self.m.update(Utils.h_list(lst))
The downside of the implementation is that it absolutely requires all build directory
files to be declared within the current build.
"""
bld = self.generator.bld
lst = [bld.task_sigs[bld.node_sigs[node]] for node in (self.inputs + self.dep_nodes) if node.is_bld()]
self.m.update(Utils.h_list(lst))
2011-09-10 11:13:51 +02:00
def sig_vars(self):
"""
2016-06-25 16:23:06 +02:00
Used by :py:meth:`waflib.Task.Task.signature`; it hashes :py:attr:`waflib.Task.Task.env` variables/values
2011-09-10 11:13:51 +02:00
"""
2016-12-07 19:04:46 +01:00
sig = self.generator.bld.hash_env_vars(self.env, self.vars)
self.m.update(sig)
2011-09-10 11:13:51 +02:00
scan = None
"""
This method, when provided, returns a tuple containing:
* a list of nodes corresponding to real files
* a list of names for files not found in path_lst
For example::
from waflib.Task import Task
class mytask(Task):
def scan(self, node):
2016-06-25 16:23:06 +02:00
return ([], [])
2011-09-10 11:13:51 +02:00
2016-06-25 16:23:06 +02:00
The first and second lists in the tuple are stored in :py:attr:`waflib.Build.BuildContext.node_deps` and
2011-09-10 11:13:51 +02:00
:py:attr:`waflib.Build.BuildContext.raw_deps` respectively.
"""
def sig_implicit_deps(self):
"""
2016-06-25 16:23:06 +02:00
Used by :py:meth:`waflib.Task.Task.signature`; it hashes node signatures
obtained by scanning for dependencies (:py:meth:`waflib.Task.Task.scan`).
2011-09-10 11:13:51 +02:00
The exception :py:class:`waflib.Errors.TaskRescan` is thrown
2016-06-25 16:23:06 +02:00
when a file has changed. In this case, the method :py:meth:`waflib.Task.Task.signature` is called
once again, and return here to call :py:meth:`waflib.Task.Task.scan` and searching for dependencies.
2011-09-10 11:13:51 +02:00
"""
bld = self.generator.bld
# get the task signatures from previous runs
key = self.uid()
prev = bld.imp_sigs.get(key, [])
2011-09-10 11:13:51 +02:00
# for issue #379
if prev:
try:
if prev == self.compute_sig_implicit_deps():
return prev
2014-10-28 22:15:52 +01:00
except Errors.TaskNotReady:
raise
2014-10-30 19:09:53 +01:00
except EnvironmentError:
# when a file was renamed, remove the stale nodes (headers in folders without source files)
2011-09-10 11:13:51 +02:00
# this will break the order calculation for headers created during the build in the source directory (should be uncommon)
# the behaviour will differ when top != out
for x in bld.node_deps.get(self.uid(), []):
2016-02-09 22:12:39 +01:00
if not x.is_bld() and not x.exists():
2011-09-10 11:13:51 +02:00
try:
2016-02-09 22:12:39 +01:00
del x.parent.children[x.name]
except KeyError:
pass
del bld.imp_sigs[key]
2011-09-10 11:13:51 +02:00
raise Errors.TaskRescan('rescan')
# no previous run or the signature of the dependencies has changed, rescan the dependencies
(bld.node_deps[key], bld.raw_deps[key]) = self.scan()
2011-09-10 11:13:51 +02:00
if Logs.verbose:
2016-06-11 22:26:34 +02:00
Logs.debug('deps: scanner for %s: %r; unresolved: %r', self, bld.node_deps[key], bld.raw_deps[key])
2011-09-10 11:13:51 +02:00
# recompute the signature and return it
try:
2016-03-25 12:03:45 +01:00
bld.imp_sigs[key] = self.compute_sig_implicit_deps()
2016-06-04 09:33:13 +02:00
except EnvironmentError:
for k in bld.node_deps.get(self.uid(), []):
if not k.exists():
Logs.warn('Dependency %r for %r is missing: check the task declaration and the build order!', k, self)
raise
2011-09-10 11:13:51 +02:00
def compute_sig_implicit_deps(self):
"""
Used by :py:meth:`waflib.Task.Task.sig_implicit_deps` for computing the actual hash of the
:py:class:`waflib.Node.Node` returned by the scanner.
2016-06-25 16:23:06 +02:00
:return: a hash value for the implicit dependencies
:rtype: string or bytes
2011-09-10 11:13:51 +02:00
"""
upd = self.m.update
self.are_implicit_nodes_ready()
# scanner returns a node that does not have a signature
# just *ignore* the error and let them figure out from the compiler output
# waf -k behaviour
for k in self.generator.bld.node_deps.get(self.uid(), []):
2011-09-10 11:13:51 +02:00
upd(k.get_bld_sig())
return self.m.digest()
def are_implicit_nodes_ready(self):
"""
2016-06-25 16:23:06 +02:00
For each node returned by the scanner, see if there is a task that creates it,
and infer the build order
2011-09-10 11:13:51 +02:00
2016-06-25 16:23:06 +02:00
This has a low performance impact on null builds (1.86s->1.66s) thanks to caching (28s->1.86s)
2011-09-10 11:13:51 +02:00
"""
bld = self.generator.bld
try:
cache = bld.dct_implicit_nodes
2012-02-11 14:43:07 +01:00
except AttributeError:
2011-09-10 11:13:51 +02:00
bld.dct_implicit_nodes = cache = {}
# one cache per build group
2011-09-10 11:13:51 +02:00
try:
dct = cache[bld.current_group]
2011-09-10 11:13:51 +02:00
except KeyError:
dct = cache[bld.current_group] = {}
2011-09-10 11:13:51 +02:00
for tsk in bld.cur_tasks:
for x in tsk.outputs:
dct[x] = tsk
modified = False
for x in bld.node_deps.get(self.uid(), []):
if x in dct:
self.run_after.add(dct[x])
modified = True
if modified:
for tsk in self.run_after:
if not tsk.hasrun:
#print "task is not ready..."
raise Errors.TaskNotReady('not ready')
if sys.hexversion > 0x3000000:
def uid(self):
try:
return self.uid_
except AttributeError:
2017-01-21 13:28:06 +01:00
m = Utils.md5(self.__class__.__name__.encode('latin-1', 'xmlcharrefreplace'))
up = m.update
for x in self.inputs + self.outputs:
2017-01-21 13:28:06 +01:00
up(x.abspath().encode('latin-1', 'xmlcharrefreplace'))
self.uid_ = m.digest()
return self.uid_
uid.__doc__ = Task.uid.__doc__
Task.uid = uid
2011-09-10 11:13:51 +02:00
def is_before(t1, t2):
"""
2016-06-25 16:23:06 +02:00
Returns a non-zero value if task t1 is to be executed before task t2::
2011-09-10 11:13:51 +02:00
t1.ext_out = '.h'
t2.ext_in = '.h'
t2.after = ['t1']
t1.before = ['t2']
waflib.Task.is_before(t1, t2) # True
2016-06-25 16:23:06 +02:00
:param t1: Task object
:type t1: :py:class:`waflib.Task.Task`
2016-06-25 16:23:06 +02:00
:param t2: Task object
:type t2: :py:class:`waflib.Task.Task`
2011-09-10 11:13:51 +02:00
"""
to_list = Utils.to_list
for k in to_list(t2.ext_in):
if k in to_list(t1.ext_out):
return 1
if t1.__class__.__name__ in to_list(t2.after):
return 1
if t2.__class__.__name__ in to_list(t1.before):
return 1
return 0
def set_file_constraints(tasks):
"""
2016-06-25 16:23:06 +02:00
Updates the ``run_after`` attribute of all tasks based on the task inputs and outputs
2011-09-10 11:13:51 +02:00
:param tasks: tasks
:type tasks: list of :py:class:`waflib.Task.Task`
2011-09-10 11:13:51 +02:00
"""
ins = Utils.defaultdict(set)
outs = Utils.defaultdict(set)
for x in tasks:
for a in x.inputs:
ins[a].add(x)
for a in x.dep_nodes:
ins[a].add(x)
for a in x.outputs:
outs[a].add(x)
2011-09-10 11:13:51 +02:00
links = set(ins.keys()).intersection(outs.keys())
for k in links:
for a in ins[k]:
a.run_after.update(outs[k])
class TaskGroup(object):
"""
Wrap nxm task order constraints into a single object
to prevent the creation of large list/set objects
This is an optimization
"""
2017-03-02 20:43:35 +01:00
def __init__(self, prev, next):
self.prev = prev
self.next = next
2017-02-20 19:01:33 +01:00
self.done = False
def get_hasrun(self):
2017-03-02 20:43:35 +01:00
for k in self.prev:
if not k.hasrun:
return NOT_RUN
return SUCCESS
hasrun = property(get_hasrun, None)
2011-09-10 11:13:51 +02:00
def set_precedence_constraints(tasks):
"""
2016-06-25 16:23:06 +02:00
Updates the ``run_after`` attribute of all tasks based on the after/before/ext_out/ext_in attributes
2011-09-10 11:13:51 +02:00
:param tasks: tasks
:type tasks: list of :py:class:`waflib.Task.Task`
2011-09-10 11:13:51 +02:00
"""
cstr_groups = Utils.defaultdict(list)
for x in tasks:
h = x.hash_constraints()
cstr_groups[h].append(x)
keys = list(cstr_groups.keys())
maxi = len(keys)
# this list should be short
for i in range(maxi):
t1 = cstr_groups[keys[i]][0]
for j in range(i + 1, maxi):
t2 = cstr_groups[keys[j]][0]
# add the constraints based on the comparisons
if is_before(t1, t2):
a = i
b = j
elif is_before(t2, t1):
a = j
b = i
else:
continue
2012-07-31 09:49:20 +02:00
a = cstr_groups[keys[a]]
b = cstr_groups[keys[b]]
if len(a) < 2 or len(b) < 2:
for x in b:
x.run_after.update(a)
else:
group = TaskGroup(set(a), set(b))
for x in b:
x.run_after.add(group)
2011-09-10 11:13:51 +02:00
def funex(c):
"""
2016-06-25 16:23:06 +02:00
Compiles a scriptlet expression into a Python function
2011-09-10 11:13:51 +02:00
:param c: function to compile
:type c: string
:return: the function 'f' declared in the input string
:rtype: function
"""
dc = {}
exec(c, dc)
return dc['f']
2016-06-05 00:23:57 +02:00
re_cond = re.compile('(?P<var>\w+)|(?P<or>\|)|(?P<and>&)')
re_novar = re.compile(r'^(SRC|TGT)\W+.*?$')
reg_act = re.compile(r'(?P<backslash>\\)|(?P<dollar>\$\$)|(?P<subst>\$\{(?P<var>\w+)(?P<code>.*?)\})', re.M)
2011-09-10 11:13:51 +02:00
def compile_fun_shell(line):
"""
2016-06-25 16:23:06 +02:00
Creates a compiled function to execute a process through a sub-shell
2011-09-10 11:13:51 +02:00
"""
extr = []
def repl(match):
g = match.group
2016-06-25 16:23:06 +02:00
if g('dollar'):
return "$"
elif g('backslash'):
return '\\\\'
elif g('subst'):
extr.append((g('var'), g('code')))
return "%s"
2011-09-10 11:13:51 +02:00
return None
line = reg_act.sub(repl, line) or line
2016-12-22 12:01:00 +01:00
dvars = []
2011-09-10 11:13:51 +02:00
2016-04-08 18:37:09 +02:00
def replc(m):
# performs substitutions and populates dvars
if m.group('and'):
return ' and '
elif m.group('or'):
return ' or '
else:
x = m.group('var')
if x not in dvars:
dvars.append(x)
return 'env[%r]' % x
2011-09-10 11:13:51 +02:00
parm = []
app = parm.append
for (var, meth) in extr:
if var == 'SRC':
if meth:
app('tsk.inputs%s' % meth)
else:
app('" ".join([a.path_from(cwdx) for a in tsk.inputs])')
2011-09-10 11:13:51 +02:00
elif var == 'TGT':
if meth:
app('tsk.outputs%s' % meth)
else:
app('" ".join([a.path_from(cwdx) for a in tsk.outputs])')
2011-09-10 11:13:51 +02:00
elif meth:
if meth.startswith(':'):
if var not in dvars:
dvars.append(var)
2011-09-10 11:13:51 +02:00
m = meth[1:]
if m == 'SRC':
m = '[a.path_from(cwdx) for a in tsk.inputs]'
2011-09-10 11:13:51 +02:00
elif m == 'TGT':
m = '[a.path_from(cwdx) for a in tsk.outputs]'
elif re_novar.match(m):
m = '[tsk.inputs%s]' % m[3:]
elif re_novar.match(m):
m = '[tsk.outputs%s]' % m[3:]
2011-09-10 11:13:51 +02:00
elif m[:3] not in ('tsk', 'gen', 'bld'):
dvars.append(meth[1:])
2011-09-10 11:13:51 +02:00
m = '%r' % m
app('" ".join(tsk.colon(%r, %s))' % (var, m))
2016-04-08 18:37:09 +02:00
elif meth.startswith('?'):
# In A?B|C output env.A if one of env.B or env.C is non-empty
expr = re_cond.sub(replc, meth[1:])
app('p(%r) if (%s) else ""' % (var, expr))
2011-09-10 11:13:51 +02:00
else:
app('%s%s' % (var, meth))
else:
if var not in dvars:
dvars.append(var)
2011-09-10 11:13:51 +02:00
app("p('%s')" % var)
if parm:
parm = "%% (%s) " % (',\n\t\t'.join(parm))
else:
parm = ''
2011-09-10 11:13:51 +02:00
c = COMPILE_TEMPLATE_SHELL % (line, parm)
2016-03-19 14:46:22 +01:00
Logs.debug('action: %s', c.strip().splitlines())
2011-09-10 11:13:51 +02:00
return (funex(c), dvars)
reg_act_noshell = re.compile(r"(?P<space>\s+)|(?P<subst>\$\{(?P<var>\w+)(?P<code>.*?)\})|(?P<text>([^$ \t\n\r\f\v]|\$\$)+)", re.M)
2011-09-10 11:13:51 +02:00
def compile_fun_noshell(line):
"""
2016-06-25 16:23:06 +02:00
Creates a compiled function to execute a process without a sub-shell
2011-09-10 11:13:51 +02:00
"""
buf = []
dvars = []
merge = False
2011-09-10 11:13:51 +02:00
app = buf.append
2016-04-08 18:37:09 +02:00
def replc(m):
# performs substitutions and populates dvars
if m.group('and'):
return ' and '
elif m.group('or'):
return ' or '
else:
x = m.group('var')
if x not in dvars:
dvars.append(x)
return 'env[%r]' % x
for m in reg_act_noshell.finditer(line):
if m.group('space'):
merge = False
continue
elif m.group('text'):
app('[%r]' % m.group('text').replace('$$', '$'))
elif m.group('subst'):
var = m.group('var')
code = m.group('code')
if var == 'SRC':
if code:
app('[tsk.inputs%s]' % code)
else:
2016-06-05 00:23:57 +02:00
app('[a.path_from(cwdx) for a in tsk.inputs]')
elif var == 'TGT':
if code:
app('[tsk.outputs%s]' % code)
else:
2016-06-05 00:23:57 +02:00
app('[a.path_from(cwdx) for a in tsk.outputs]')
elif code:
if code.startswith(':'):
# a composed variable ${FOO:OUT}
if not var in dvars:
dvars.append(var)
m = code[1:]
if m == 'SRC':
m = '[a.path_from(cwdx) for a in tsk.inputs]'
elif m == 'TGT':
m = '[a.path_from(cwdx) for a in tsk.outputs]'
elif re_novar.match(m):
m = '[tsk.inputs%s]' % m[3:]
elif re_novar.match(m):
m = '[tsk.outputs%s]' % m[3:]
elif m[:3] not in ('tsk', 'gen', 'bld'):
dvars.append(m)
m = '%r' % m
app('tsk.colon(%r, %s)' % (var, m))
2016-04-08 18:37:09 +02:00
elif code.startswith('?'):
# In A?B|C output env.A if one of env.B or env.C is non-empty
expr = re_cond.sub(replc, code[1:])
app('to_list(env[%r] if (%s) else [])' % (var, expr))
else:
# plain code such as ${tsk.inputs[0].abspath()}
app('gen.to_list(%s%s)' % (var, code))
2011-09-10 11:13:51 +02:00
else:
# a plain variable such as # a plain variable like ${AR}
app('to_list(env[%r])' % var)
if not var in dvars:
dvars.append(var)
if merge:
tmp = 'merge(%s, %s)' % (buf[-2], buf[-1])
del buf[-1]
buf[-1] = tmp
merge = True # next turn
buf = ['lst.extend(%s)' % x for x in buf]
2011-09-10 11:13:51 +02:00
fun = COMPILE_TEMPLATE_NOSHELL % "\n\t".join(buf)
2016-03-19 14:46:22 +01:00
Logs.debug('action: %s', fun.strip().splitlines())
2011-09-10 11:13:51 +02:00
return (funex(fun), dvars)
def compile_fun(line, shell=False):
"""
2016-06-25 16:23:06 +02:00
Parses a string expression such as '${CC} ${SRC} -o ${TGT}' and returns a pair containing:
2011-09-10 11:13:51 +02:00
* The function created (compiled) for use as :py:meth:`waflib.Task.Task.run`
2016-06-25 16:23:06 +02:00
* The list of variables that must cause rebuilds when *env* data is modified
2011-09-10 11:13:51 +02:00
for example::
from waflib.Task import compile_fun
compile_fun('cxx', '${CXX} -o ${TGT[0]} ${SRC} -I ${SRC[0].parent.bldpath()}')
def build(bld):
bld(source='wscript', rule='echo "foo\\${SRC[0].name}\\bar"')
2016-06-25 16:23:06 +02:00
The env variables (CXX, ..) on the task must not hold dicts so as to preserve a consistent order.
The reserved keywords ``TGT`` and ``SRC`` represent the task input and output nodes
2011-09-10 11:13:51 +02:00
"""
if isinstance(line, str):
if line.find('<') > 0 or line.find('>') > 0 or line.find('&&') > 0:
shell = True
else:
dvars_lst = []
funs_lst = []
for x in line:
if isinstance(x, str):
fun, dvars = compile_fun(x, shell)
dvars_lst += dvars
funs_lst.append(fun)
else:
# assume a function to let through
funs_lst.append(x)
def composed_fun(task):
for x in funs_lst:
ret = x(task)
if ret:
return ret
return None
2016-12-22 12:01:00 +01:00
return composed_fun, dvars_lst
2011-09-10 11:13:51 +02:00
if shell:
return compile_fun_shell(line)
else:
return compile_fun_noshell(line)
def task_factory(name, func=None, vars=None, color='GREEN', ext_in=[], ext_out=[], before=[], after=[], shell=False, scan=None):
"""
Returns a new task subclass with the function ``run`` compiled from the line given.
2011-09-10 11:13:51 +02:00
:param func: method run
:type func: string or function
:param vars: list of variables to hash
:type vars: list of string
:param color: color to use
:type color: string
:param shell: when *func* is a string, enable/disable the use of the shell
:type shell: bool
:param scan: method scan
:type scan: function
:rtype: :py:class:`waflib.Task.Task`
"""
params = {
'vars': vars or [], # function arguments are static, and this one may be modified by the class
'color': color,
'name': name,
'shell': shell,
'scan': scan,
}
if isinstance(func, str) or isinstance(func, tuple):
2011-09-10 11:13:51 +02:00
params['run_str'] = func
else:
params['run'] = func
cls = type(Task)(name, (Task,), params)
classes[name] = cls
if ext_in:
cls.ext_in = Utils.to_list(ext_in)
if ext_out:
cls.ext_out = Utils.to_list(ext_out)
if before:
cls.before = Utils.to_list(before)
if after:
cls.after = Utils.to_list(after)
2011-09-10 11:13:51 +02:00
return cls
def deep_inputs(cls):
"""
Task class decorator to enable rebuilds on input files task signatures
"""
def sig_explicit_deps(self):
Task.sig_explicit_deps(self)
Task.sig_deep_inputs(self)
cls.sig_explicit_deps = sig_explicit_deps
return cls
TaskBase = Task
"Provided for compatibility reasons, TaskBase should not be used"