aboutsummaryrefslogtreecommitdiffstats
path: root/waflib/TaskGen.py
diff options
context:
space:
mode:
Diffstat (limited to 'waflib/TaskGen.py')
-rw-r--r--waflib/TaskGen.py917
1 files changed, 917 insertions, 0 deletions
diff --git a/waflib/TaskGen.py b/waflib/TaskGen.py
new file mode 100644
index 0000000..532b7d5
--- /dev/null
+++ b/waflib/TaskGen.py
@@ -0,0 +1,917 @@
+#!/usr/bin/env python
+# encoding: utf-8
+# Thomas Nagy, 2005-2018 (ita)
+
+"""
+Task generators
+
+The class :py:class:`waflib.TaskGen.task_gen` encapsulates the creation of task objects (low-level code)
+The instances can have various parameters, but the creation of task nodes (Task.py)
+is deferred. To achieve this, various methods are called from the method "apply"
+"""
+
+import copy, re, os, functools
+from waflib import Task, Utils, Logs, Errors, ConfigSet, Node
+
+feats = Utils.defaultdict(set)
+"""remember the methods declaring features"""
+
+HEADER_EXTS = ['.h', '.hpp', '.hxx', '.hh']
+
+class task_gen(object):
+ """
+ Instances of this class create :py:class:`waflib.Task.Task` when
+ calling the method :py:meth:`waflib.TaskGen.task_gen.post` from the main thread.
+ A few notes:
+
+ * The methods to call (*self.meths*) can be specified dynamically (removing, adding, ..)
+ * The 'features' are used to add methods to self.meths and then execute them
+ * The attribute 'path' is a node representing the location of the task generator
+ * The tasks created are added to the attribute *tasks*
+ * The attribute 'idx' is a counter of task generators in the same path
+ """
+
+ mappings = Utils.ordered_iter_dict()
+ """Mappings are global file extension mappings that are retrieved in the order of definition"""
+
+ prec = Utils.defaultdict(set)
+ """Dict that holds the precedence execution rules for task generator methods"""
+
+ def __init__(self, *k, **kw):
+ """
+ Task generator objects predefine various attributes (source, target) for possible
+ processing by process_rule (make-like rules) or process_source (extensions, misc methods)
+
+ Tasks are stored on the attribute 'tasks'. They are created by calling methods
+ listed in ``self.meths`` or referenced in the attribute ``features``
+ A topological sort is performed to execute the methods in correct order.
+
+ The extra key/value elements passed in ``kw`` are set as attributes
+ """
+ self.source = []
+ self.target = ''
+
+ self.meths = []
+ """
+ List of method names to execute (internal)
+ """
+
+ self.features = []
+ """
+ List of feature names for bringing new methods in
+ """
+
+ self.tasks = []
+ """
+ Tasks created are added to this list
+ """
+
+ if not 'bld' in kw:
+ # task generators without a build context :-/
+ self.env = ConfigSet.ConfigSet()
+ self.idx = 0
+ self.path = None
+ else:
+ self.bld = kw['bld']
+ self.env = self.bld.env.derive()
+ self.path = kw.get('path', self.bld.path) # by default, emulate chdir when reading scripts
+
+ # Provide a unique index per folder
+ # This is part of a measure to prevent output file name collisions
+ path = self.path.abspath()
+ try:
+ self.idx = self.bld.idx[path] = self.bld.idx.get(path, 0) + 1
+ except AttributeError:
+ self.bld.idx = {}
+ self.idx = self.bld.idx[path] = 1
+
+ # Record the global task generator count
+ try:
+ self.tg_idx_count = self.bld.tg_idx_count = self.bld.tg_idx_count + 1
+ except AttributeError:
+ self.tg_idx_count = self.bld.tg_idx_count = 1
+
+ for key, val in kw.items():
+ setattr(self, key, val)
+
+ def __str__(self):
+ """Debugging helper"""
+ return "<task_gen %r declared in %s>" % (self.name, self.path.abspath())
+
+ def __repr__(self):
+ """Debugging helper"""
+ lst = []
+ for x in self.__dict__:
+ if x not in ('env', 'bld', 'compiled_tasks', 'tasks'):
+ lst.append("%s=%s" % (x, repr(getattr(self, x))))
+ return "bld(%s) in %s" % (", ".join(lst), self.path.abspath())
+
+ def get_cwd(self):
+ """
+ Current working directory for the task generator, defaults to the build directory.
+ This is still used in a few places but it should disappear at some point as the classes
+ define their own working directory.
+
+ :rtype: :py:class:`waflib.Node.Node`
+ """
+ return self.bld.bldnode
+
+ def get_name(self):
+ """
+ If the attribute ``name`` is not set on the instance,
+ the name is computed from the target name::
+
+ def build(bld):
+ x = bld(name='foo')
+ x.get_name() # foo
+ y = bld(target='bar')
+ y.get_name() # bar
+
+ :rtype: string
+ :return: name of this task generator
+ """
+ try:
+ return self._name
+ except AttributeError:
+ if isinstance(self.target, list):
+ lst = [str(x) for x in self.target]
+ name = self._name = ','.join(lst)
+ else:
+ name = self._name = str(self.target)
+ return name
+ def set_name(self, name):
+ self._name = name
+
+ name = property(get_name, set_name)
+
+ def to_list(self, val):
+ """
+ Ensures that a parameter is a list, see :py:func:`waflib.Utils.to_list`
+
+ :type val: string or list of string
+ :param val: input to return as a list
+ :rtype: list
+ """
+ if isinstance(val, str):
+ return val.split()
+ else:
+ return val
+
+ def post(self):
+ """
+ Creates tasks for this task generators. The following operations are performed:
+
+ #. The body of this method is called only once and sets the attribute ``posted``
+ #. The attribute ``features`` is used to add more methods in ``self.meths``
+ #. The methods are sorted by the precedence table ``self.prec`` or `:waflib:attr:waflib.TaskGen.task_gen.prec`
+ #. The methods are then executed in order
+ #. The tasks created are added to :py:attr:`waflib.TaskGen.task_gen.tasks`
+ """
+ if getattr(self, 'posted', None):
+ return False
+ self.posted = True
+
+ keys = set(self.meths)
+ keys.update(feats['*'])
+
+ # add the methods listed in the features
+ self.features = Utils.to_list(self.features)
+ for x in self.features:
+ st = feats[x]
+ if st:
+ keys.update(st)
+ elif not x in Task.classes:
+ Logs.warn('feature %r does not exist - bind at least one method to it?', x)
+
+ # copy the precedence table
+ prec = {}
+ prec_tbl = self.prec
+ for x in prec_tbl:
+ if x in keys:
+ prec[x] = prec_tbl[x]
+
+ # elements disconnected
+ tmp = []
+ for a in keys:
+ for x in prec.values():
+ if a in x:
+ break
+ else:
+ tmp.append(a)
+
+ tmp.sort(reverse=True)
+
+ # topological sort
+ out = []
+ while tmp:
+ e = tmp.pop()
+ if e in keys:
+ out.append(e)
+ try:
+ nlst = prec[e]
+ except KeyError:
+ pass
+ else:
+ del prec[e]
+ for x in nlst:
+ for y in prec:
+ if x in prec[y]:
+ break
+ else:
+ tmp.append(x)
+ tmp.sort(reverse=True)
+
+ if prec:
+ buf = ['Cycle detected in the method execution:']
+ for k, v in prec.items():
+ buf.append('- %s after %s' % (k, [x for x in v if x in prec]))
+ raise Errors.WafError('\n'.join(buf))
+ self.meths = out
+
+ # then we run the methods in order
+ Logs.debug('task_gen: posting %s %d', self, id(self))
+ for x in out:
+ try:
+ v = getattr(self, x)
+ except AttributeError:
+ raise Errors.WafError('%r is not a valid task generator method' % x)
+ Logs.debug('task_gen: -> %s (%d)', x, id(self))
+ v()
+
+ Logs.debug('task_gen: posted %s', self.name)
+ return True
+
+ def get_hook(self, node):
+ """
+ Returns the ``@extension`` method to call for a Node of a particular extension.
+
+ :param node: Input file to process
+ :type node: :py:class:`waflib.Tools.Node.Node`
+ :return: A method able to process the input node by looking at the extension
+ :rtype: function
+ """
+ name = node.name
+ for k in self.mappings:
+ try:
+ if name.endswith(k):
+ return self.mappings[k]
+ except TypeError:
+ # regexps objects
+ if k.match(name):
+ return self.mappings[k]
+ keys = list(self.mappings.keys())
+ raise Errors.WafError("File %r has no mapping in %r (load a waf tool?)" % (node, keys))
+
+ def create_task(self, name, src=None, tgt=None, **kw):
+ """
+ Creates task instances.
+
+ :param name: task class name
+ :type name: string
+ :param src: input nodes
+ :type src: list of :py:class:`waflib.Tools.Node.Node`
+ :param tgt: output nodes
+ :type tgt: list of :py:class:`waflib.Tools.Node.Node`
+ :return: A task object
+ :rtype: :py:class:`waflib.Task.Task`
+ """
+ task = Task.classes[name](env=self.env.derive(), generator=self)
+ if src:
+ task.set_inputs(src)
+ if tgt:
+ task.set_outputs(tgt)
+ task.__dict__.update(kw)
+ self.tasks.append(task)
+ return task
+
+ def clone(self, env):
+ """
+ Makes a copy of a task generator. Once the copy is made, it is necessary to ensure that the
+ it does not create the same output files as the original, or the same files may
+ be compiled several times.
+
+ :param env: A configuration set
+ :type env: :py:class:`waflib.ConfigSet.ConfigSet`
+ :return: A copy
+ :rtype: :py:class:`waflib.TaskGen.task_gen`
+ """
+ newobj = self.bld()
+ for x in self.__dict__:
+ if x in ('env', 'bld'):
+ continue
+ elif x in ('path', 'features'):
+ setattr(newobj, x, getattr(self, x))
+ else:
+ setattr(newobj, x, copy.copy(getattr(self, x)))
+
+ newobj.posted = False
+ if isinstance(env, str):
+ newobj.env = self.bld.all_envs[env].derive()
+ else:
+ newobj.env = env.derive()
+
+ return newobj
+
+def declare_chain(name='', rule=None, reentrant=None, color='BLUE',
+ ext_in=[], ext_out=[], before=[], after=[], decider=None, scan=None, install_path=None, shell=False):
+ """
+ Creates a new mapping and a task class for processing files by extension.
+ See Tools/flex.py for an example.
+
+ :param name: name for the task class
+ :type name: string
+ :param rule: function to execute or string to be compiled in a function
+ :type rule: string or function
+ :param reentrant: re-inject the output file in the process (done automatically, set to 0 to disable)
+ :type reentrant: int
+ :param color: color for the task output
+ :type color: string
+ :param ext_in: execute the task only after the files of such extensions are created
+ :type ext_in: list of string
+ :param ext_out: execute the task only before files of such extensions are processed
+ :type ext_out: list of string
+ :param before: execute instances of this task before classes of the given names
+ :type before: list of string
+ :param after: execute instances of this task after classes of the given names
+ :type after: list of string
+ :param decider: if present, function that returns a list of output file extensions (overrides ext_out for output files, but not for the build order)
+ :type decider: function
+ :param scan: scanner function for the task
+ :type scan: function
+ :param install_path: installation path for the output nodes
+ :type install_path: string
+ """
+ ext_in = Utils.to_list(ext_in)
+ ext_out = Utils.to_list(ext_out)
+ if not name:
+ name = rule
+ cls = Task.task_factory(name, rule, color=color, ext_in=ext_in, ext_out=ext_out, before=before, after=after, scan=scan, shell=shell)
+
+ def x_file(self, node):
+ if ext_in:
+ _ext_in = ext_in[0]
+
+ tsk = self.create_task(name, node)
+ cnt = 0
+
+ ext = decider(self, node) if decider else cls.ext_out
+ for x in ext:
+ k = node.change_ext(x, ext_in=_ext_in)
+ tsk.outputs.append(k)
+
+ if reentrant != None:
+ if cnt < int(reentrant):
+ self.source.append(k)
+ else:
+ # reinject downstream files into the build
+ for y in self.mappings: # ~ nfile * nextensions :-/
+ if k.name.endswith(y):
+ self.source.append(k)
+ break
+ cnt += 1
+
+ if install_path:
+ self.install_task = self.add_install_files(install_to=install_path, install_from=tsk.outputs)
+ return tsk
+
+ for x in cls.ext_in:
+ task_gen.mappings[x] = x_file
+ return x_file
+
+def taskgen_method(func):
+ """
+ Decorator that registers method as a task generator method.
+ The function must accept a task generator as first parameter::
+
+ from waflib.TaskGen import taskgen_method
+ @taskgen_method
+ def mymethod(self):
+ pass
+
+ :param func: task generator method to add
+ :type func: function
+ :rtype: function
+ """
+ setattr(task_gen, func.__name__, func)
+ return func
+
+def feature(*k):
+ """
+ Decorator that registers a task generator method that will be executed when the
+ object attribute ``feature`` contains the corresponding key(s)::
+
+ from waflib.Task import feature
+ @feature('myfeature')
+ def myfunction(self):
+ print('that is my feature!')
+ def build(bld):
+ bld(features='myfeature')
+
+ :param k: feature names
+ :type k: list of string
+ """
+ def deco(func):
+ setattr(task_gen, func.__name__, func)
+ for name in k:
+ feats[name].update([func.__name__])
+ return func
+ return deco
+
+def before_method(*k):
+ """
+ Decorator that registera task generator method which will be executed
+ before the functions of given name(s)::
+
+ from waflib.TaskGen import feature, before
+ @feature('myfeature')
+ @before_method('fun2')
+ def fun1(self):
+ print('feature 1!')
+ @feature('myfeature')
+ def fun2(self):
+ print('feature 2!')
+ def build(bld):
+ bld(features='myfeature')
+
+ :param k: method names
+ :type k: list of string
+ """
+ def deco(func):
+ setattr(task_gen, func.__name__, func)
+ for fun_name in k:
+ task_gen.prec[func.__name__].add(fun_name)
+ return func
+ return deco
+before = before_method
+
+def after_method(*k):
+ """
+ Decorator that registers a task generator method which will be executed
+ after the functions of given name(s)::
+
+ from waflib.TaskGen import feature, after
+ @feature('myfeature')
+ @after_method('fun2')
+ def fun1(self):
+ print('feature 1!')
+ @feature('myfeature')
+ def fun2(self):
+ print('feature 2!')
+ def build(bld):
+ bld(features='myfeature')
+
+ :param k: method names
+ :type k: list of string
+ """
+ def deco(func):
+ setattr(task_gen, func.__name__, func)
+ for fun_name in k:
+ task_gen.prec[fun_name].add(func.__name__)
+ return func
+ return deco
+after = after_method
+
+def extension(*k):
+ """
+ Decorator that registers a task generator method which will be invoked during
+ the processing of source files for the extension given::
+
+ from waflib import Task
+ class mytask(Task):
+ run_str = 'cp ${SRC} ${TGT}'
+ @extension('.moo')
+ def create_maa_file(self, node):
+ self.create_task('mytask', node, node.change_ext('.maa'))
+ def build(bld):
+ bld(source='foo.moo')
+ """
+ def deco(func):
+ setattr(task_gen, func.__name__, func)
+ for x in k:
+ task_gen.mappings[x] = func
+ return func
+ return deco
+
+@taskgen_method
+def to_nodes(self, lst, path=None):
+ """
+ Flatten the input list of string/nodes/lists into a list of nodes.
+
+ It is used by :py:func:`waflib.TaskGen.process_source` and :py:func:`waflib.TaskGen.process_rule`.
+ It is designed for source files, for folders, see :py:func:`waflib.Tools.ccroot.to_incnodes`:
+
+ :param lst: input list
+ :type lst: list of string and nodes
+ :param path: path from which to search the nodes (by default, :py:attr:`waflib.TaskGen.task_gen.path`)
+ :type path: :py:class:`waflib.Tools.Node.Node`
+ :rtype: list of :py:class:`waflib.Tools.Node.Node`
+ """
+ tmp = []
+ path = path or self.path
+ find = path.find_resource
+
+ if isinstance(lst, Node.Node):
+ lst = [lst]
+
+ for x in Utils.to_list(lst):
+ if isinstance(x, str):
+ node = find(x)
+ elif hasattr(x, 'name'):
+ node = x
+ else:
+ tmp.extend(self.to_nodes(x))
+ continue
+ if not node:
+ raise Errors.WafError('source not found: %r in %r' % (x, self))
+ tmp.append(node)
+ return tmp
+
+@feature('*')
+def process_source(self):
+ """
+ Processes each element in the attribute ``source`` by extension.
+
+ #. The *source* list is converted through :py:meth:`waflib.TaskGen.to_nodes` to a list of :py:class:`waflib.Node.Node` first.
+ #. File extensions are mapped to methods having the signature: ``def meth(self, node)`` by :py:meth:`waflib.TaskGen.extension`
+ #. The method is retrieved through :py:meth:`waflib.TaskGen.task_gen.get_hook`
+ #. When called, the methods may modify self.source to append more source to process
+ #. The mappings can map an extension or a filename (see the code below)
+ """
+ self.source = self.to_nodes(getattr(self, 'source', []))
+ for node in self.source:
+ self.get_hook(node)(self, node)
+
+@feature('*')
+@before_method('process_source')
+def process_rule(self):
+ """
+ Processes the attribute ``rule``. When present, :py:meth:`waflib.TaskGen.process_source` is disabled::
+
+ def build(bld):
+ bld(rule='cp ${SRC} ${TGT}', source='wscript', target='bar.txt')
+
+ Main attributes processed:
+
+ * rule: command to execute, it can be a tuple of strings for multiple commands
+ * chmod: permissions for the resulting files (integer value such as Utils.O755)
+ * shell: set to False to execute the command directly (default is True to use a shell)
+ * scan: scanner function
+ * vars: list of variables to trigger rebuilds, such as CFLAGS
+ * cls_str: string to display when executing the task
+ * cls_keyword: label to display when executing the task
+ * cache_rule: by default, try to re-use similar classes, set to False to disable
+ * source: list of Node or string objects representing the source files required by this task
+ * target: list of Node or string objects representing the files that this task creates
+ * cwd: current working directory (Node or string)
+ * stdout: standard output, set to None to prevent waf from capturing the text
+ * stderr: standard error, set to None to prevent waf from capturing the text
+ * timeout: timeout for command execution (Python 3)
+ * always: whether to always run the command (False by default)
+ * deep_inputs: whether the task must depend on the input file tasks too (False by default)
+ """
+ if not getattr(self, 'rule', None):
+ return
+
+ # create the task class
+ name = str(getattr(self, 'name', None) or self.target or getattr(self.rule, '__name__', self.rule))
+
+ # or we can put the class in a cache for performance reasons
+ try:
+ cache = self.bld.cache_rule_attr
+ except AttributeError:
+ cache = self.bld.cache_rule_attr = {}
+
+ chmod = getattr(self, 'chmod', None)
+ shell = getattr(self, 'shell', True)
+ color = getattr(self, 'color', 'BLUE')
+ scan = getattr(self, 'scan', None)
+ _vars = getattr(self, 'vars', [])
+ cls_str = getattr(self, 'cls_str', None)
+ cls_keyword = getattr(self, 'cls_keyword', None)
+ use_cache = getattr(self, 'cache_rule', 'True')
+ deep_inputs = getattr(self, 'deep_inputs', False)
+
+ scan_val = has_deps = hasattr(self, 'deps')
+ if scan:
+ scan_val = id(scan)
+
+ key = Utils.h_list((name, self.rule, chmod, shell, color, cls_str, cls_keyword, scan_val, _vars, deep_inputs))
+
+ cls = None
+ if use_cache:
+ try:
+ cls = cache[key]
+ except KeyError:
+ pass
+ if not cls:
+ rule = self.rule
+ if chmod is not None:
+ def chmod_fun(tsk):
+ for x in tsk.outputs:
+ os.chmod(x.abspath(), tsk.generator.chmod)
+ if isinstance(rule, tuple):
+ rule = list(rule)
+ rule.append(chmod_fun)
+ rule = tuple(rule)
+ else:
+ rule = (rule, chmod_fun)
+
+ cls = Task.task_factory(name, rule, _vars, shell=shell, color=color)
+
+ if cls_str:
+ setattr(cls, '__str__', self.cls_str)
+
+ if cls_keyword:
+ setattr(cls, 'keyword', self.cls_keyword)
+
+ if deep_inputs:
+ Task.deep_inputs(cls)
+
+ if scan:
+ cls.scan = self.scan
+ elif has_deps:
+ def scan(self):
+ nodes = []
+ for x in self.generator.to_list(getattr(self.generator, 'deps', None)):
+ node = self.generator.path.find_resource(x)
+ if not node:
+ self.generator.bld.fatal('Could not find %r (was it declared?)' % x)
+ nodes.append(node)
+ return [nodes, []]
+ cls.scan = scan
+
+ if use_cache:
+ cache[key] = cls
+
+ # now create one instance
+ tsk = self.create_task(name)
+
+ for x in ('after', 'before', 'ext_in', 'ext_out'):
+ setattr(tsk, x, getattr(self, x, []))
+
+ if hasattr(self, 'stdout'):
+ tsk.stdout = self.stdout
+
+ if hasattr(self, 'stderr'):
+ tsk.stderr = self.stderr
+
+ if getattr(self, 'timeout', None):
+ tsk.timeout = self.timeout
+
+ if getattr(self, 'always', None):
+ tsk.always_run = True
+
+ if getattr(self, 'target', None):
+ if isinstance(self.target, str):
+ self.target = self.target.split()
+ if not isinstance(self.target, list):
+ self.target = [self.target]
+ for x in self.target:
+ if isinstance(x, str):
+ tsk.outputs.append(self.path.find_or_declare(x))
+ else:
+ x.parent.mkdir() # if a node was given, create the required folders
+ tsk.outputs.append(x)
+ if getattr(self, 'install_path', None):
+ self.install_task = self.add_install_files(install_to=self.install_path,
+ install_from=tsk.outputs, chmod=getattr(self, 'chmod', Utils.O644))
+
+ if getattr(self, 'source', None):
+ tsk.inputs = self.to_nodes(self.source)
+ # bypass the execution of process_source by setting the source to an empty list
+ self.source = []
+
+ if getattr(self, 'cwd', None):
+ tsk.cwd = self.cwd
+
+ if isinstance(tsk.run, functools.partial):
+ # Python documentation says: "partial objects defined in classes
+ # behave like static methods and do not transform into bound
+ # methods during instance attribute look-up."
+ tsk.run = functools.partial(tsk.run, tsk)
+
+@feature('seq')
+def sequence_order(self):
+ """
+ Adds a strict sequential constraint between the tasks generated by task generators.
+ It works because task generators are posted in order.
+ It will not post objects which belong to other folders.
+
+ Example::
+
+ bld(features='javac seq')
+ bld(features='jar seq')
+
+ To start a new sequence, set the attribute seq_start, for example::
+
+ obj = bld(features='seq')
+ obj.seq_start = True
+
+ Note that the method is executed in last position. This is more an
+ example than a widely-used solution.
+ """
+ if self.meths and self.meths[-1] != 'sequence_order':
+ self.meths.append('sequence_order')
+ return
+
+ if getattr(self, 'seq_start', None):
+ return
+
+ # all the tasks previously declared must be run before these
+ if getattr(self.bld, 'prev', None):
+ self.bld.prev.post()
+ for x in self.bld.prev.tasks:
+ for y in self.tasks:
+ y.set_run_after(x)
+
+ self.bld.prev = self
+
+
+re_m4 = re.compile(r'@(\w+)@', re.M)
+
+class subst_pc(Task.Task):
+ """
+ Creates *.pc* files from *.pc.in*. The task is executed whenever an input variable used
+ in the substitution changes.
+ """
+
+ def force_permissions(self):
+ "Private for the time being, we will probably refactor this into run_str=[run1,chmod]"
+ if getattr(self.generator, 'chmod', None):
+ for x in self.outputs:
+ os.chmod(x.abspath(), self.generator.chmod)
+
+ def run(self):
+ "Substitutes variables in a .in file"
+
+ if getattr(self.generator, 'is_copy', None):
+ for i, x in enumerate(self.outputs):
+ x.write(self.inputs[i].read('rb'), 'wb')
+ stat = os.stat(self.inputs[i].abspath()) # Preserve mtime of the copy
+ os.utime(self.outputs[i].abspath(), (stat.st_atime, stat.st_mtime))
+ self.force_permissions()
+ return None
+
+ if getattr(self.generator, 'fun', None):
+ ret = self.generator.fun(self)
+ if not ret:
+ self.force_permissions()
+ return ret
+
+ code = self.inputs[0].read(encoding=getattr(self.generator, 'encoding', 'latin-1'))
+ if getattr(self.generator, 'subst_fun', None):
+ code = self.generator.subst_fun(self, code)
+ if code is not None:
+ self.outputs[0].write(code, encoding=getattr(self.generator, 'encoding', 'latin-1'))
+ self.force_permissions()
+ return None
+
+ # replace all % by %% to prevent errors by % signs
+ code = code.replace('%', '%%')
+
+ # extract the vars foo into lst and replace @foo@ by %(foo)s
+ lst = []
+ def repl(match):
+ g = match.group
+ if g(1):
+ lst.append(g(1))
+ return "%%(%s)s" % g(1)
+ return ''
+ code = getattr(self.generator, 're_m4', re_m4).sub(repl, code)
+
+ try:
+ d = self.generator.dct
+ except AttributeError:
+ d = {}
+ for x in lst:
+ tmp = getattr(self.generator, x, '') or self.env[x] or self.env[x.upper()]
+ try:
+ tmp = ''.join(tmp)
+ except TypeError:
+ tmp = str(tmp)
+ d[x] = tmp
+
+ code = code % d
+ self.outputs[0].write(code, encoding=getattr(self.generator, 'encoding', 'latin-1'))
+ self.generator.bld.raw_deps[self.uid()] = lst
+
+ # make sure the signature is updated
+ try:
+ delattr(self, 'cache_sig')
+ except AttributeError:
+ pass
+
+ self.force_permissions()
+
+ def sig_vars(self):
+ """
+ Compute a hash (signature) of the variables used in the substitution
+ """
+ bld = self.generator.bld
+ env = self.env
+ upd = self.m.update
+
+ if getattr(self.generator, 'fun', None):
+ upd(Utils.h_fun(self.generator.fun).encode())
+ if getattr(self.generator, 'subst_fun', None):
+ upd(Utils.h_fun(self.generator.subst_fun).encode())
+
+ # raw_deps: persistent custom values returned by the scanner
+ vars = self.generator.bld.raw_deps.get(self.uid(), [])
+
+ # hash both env vars and task generator attributes
+ act_sig = bld.hash_env_vars(env, vars)
+ upd(act_sig)
+
+ lst = [getattr(self.generator, x, '') for x in vars]
+ upd(Utils.h_list(lst))
+
+ return self.m.digest()
+
+@extension('.pc.in')
+def add_pcfile(self, node):
+ """
+ Processes *.pc.in* files to *.pc*. Installs the results to ``${PREFIX}/lib/pkgconfig/`` by default
+
+ def build(bld):
+ bld(source='foo.pc.in', install_path='${LIBDIR}/pkgconfig/')
+ """
+ tsk = self.create_task('subst_pc', node, node.change_ext('.pc', '.pc.in'))
+ self.install_task = self.add_install_files(
+ install_to=getattr(self, 'install_path', '${LIBDIR}/pkgconfig/'), install_from=tsk.outputs)
+
+class subst(subst_pc):
+ pass
+
+@feature('subst')
+@before_method('process_source', 'process_rule')
+def process_subst(self):
+ """
+ Defines a transformation that substitutes the contents of *source* files to *target* files::
+
+ def build(bld):
+ bld(
+ features='subst',
+ source='foo.c.in',
+ target='foo.c',
+ install_path='${LIBDIR}/pkgconfig',
+ VAR = 'val'
+ )
+
+ The input files are supposed to contain macros of the form *@VAR@*, where *VAR* is an argument
+ of the task generator object.
+
+ This method overrides the processing by :py:meth:`waflib.TaskGen.process_source`.
+ """
+
+ src = Utils.to_list(getattr(self, 'source', []))
+ if isinstance(src, Node.Node):
+ src = [src]
+ tgt = Utils.to_list(getattr(self, 'target', []))
+ if isinstance(tgt, Node.Node):
+ tgt = [tgt]
+ if len(src) != len(tgt):
+ raise Errors.WafError('invalid number of source/target for %r' % self)
+
+ for x, y in zip(src, tgt):
+ if not x or not y:
+ raise Errors.WafError('null source or target for %r' % self)
+ a, b = None, None
+
+ if isinstance(x, str) and isinstance(y, str) and x == y:
+ a = self.path.find_node(x)
+ b = self.path.get_bld().make_node(y)
+ if not os.path.isfile(b.abspath()):
+ b.parent.mkdir()
+ else:
+ if isinstance(x, str):
+ a = self.path.find_resource(x)
+ elif isinstance(x, Node.Node):
+ a = x
+ if isinstance(y, str):
+ b = self.path.find_or_declare(y)
+ elif isinstance(y, Node.Node):
+ b = y
+
+ if not a:
+ raise Errors.WafError('could not find %r for %r' % (x, self))
+
+ tsk = self.create_task('subst', a, b)
+ for k in ('after', 'before', 'ext_in', 'ext_out'):
+ val = getattr(self, k, None)
+ if val:
+ setattr(tsk, k, val)
+
+ # paranoid safety measure for the general case foo.in->foo.h with ambiguous dependencies
+ for xt in HEADER_EXTS:
+ if b.name.endswith(xt):
+ tsk.ext_in = tsk.ext_in + ['.h']
+ break
+
+ inst_to = getattr(self, 'install_path', None)
+ if inst_to:
+ self.install_task = self.add_install_files(install_to=inst_to,
+ install_from=b, chmod=getattr(self, 'chmod', Utils.O644))
+
+ self.source = []
+