3 # Thomas Nagy, 2005-2018 (ita)
8 The class :py:class:`waflib.TaskGen.task_gen` encapsulates the creation of task objects (low-level code)
9 The instances can have various parameters, but the creation of task nodes (Task.py)
10 is deferred. To achieve this, various methods are called from the method "apply"
13 import copy
, re
, os
, functools
14 from waflib
import Task
, Utils
, Logs
, Errors
, ConfigSet
, Node
16 feats
= Utils
.defaultdict(set)
17 """remember the methods declaring features"""
19 HEADER_EXTS
= ['.h', '.hpp', '.hxx', '.hh']
21 class task_gen(object):
23 Instances of this class create :py:class:`waflib.Task.Task` when
24 calling the method :py:meth:`waflib.TaskGen.task_gen.post` from the main thread.
27 * The methods to call (*self.meths*) can be specified dynamically (removing, adding, ..)
28 * The 'features' are used to add methods to self.meths and then execute them
29 * The attribute 'path' is a node representing the location of the task generator
30 * The tasks created are added to the attribute *tasks*
31 * The attribute 'idx' is a counter of task generators in the same path
34 mappings
= Utils
.ordered_iter_dict()
35 """Mappings are global file extension mappings that are retrieved in the order of definition"""
37 prec
= Utils
.defaultdict(set)
38 """Dict that holds the precedence execution rules for task generator methods"""
40 def __init__(self
, *k
, **kw
):
42 Task generator objects predefine various attributes (source, target) for possible
43 processing by process_rule (make-like rules) or process_source (extensions, misc methods)
45 Tasks are stored on the attribute 'tasks'. They are created by calling methods
46 listed in ``self.meths`` or referenced in the attribute ``features``
47 A topological sort is performed to execute the methods in correct order.
49 The extra key/value elements passed in ``kw`` are set as attributes
56 List of method names to execute (internal)
61 List of feature names for bringing new methods in
66 Tasks created are added to this list
70 # task generators without a build context :-/
71 self
.env
= ConfigSet
.ConfigSet()
76 self
.env
= self
.bld
.env
.derive()
77 self
.path
= kw
.get('path', self
.bld
.path
) # by default, emulate chdir when reading scripts
79 # Provide a unique index per folder
80 # This is part of a measure to prevent output file name collisions
81 path
= self
.path
.abspath()
83 self
.idx
= self
.bld
.idx
[path
] = self
.bld
.idx
.get(path
, 0) + 1
84 except AttributeError:
86 self
.idx
= self
.bld
.idx
[path
] = 1
88 # Record the global task generator count
90 self
.tg_idx_count
= self
.bld
.tg_idx_count
= self
.bld
.tg_idx_count
+ 1
91 except AttributeError:
92 self
.tg_idx_count
= self
.bld
.tg_idx_count
= 1
94 for key
, val
in kw
.items():
95 setattr(self
, key
, val
)
98 """Debugging helper"""
99 return "<task_gen %r declared in %s>" % (self
.name
, self
.path
.abspath())
102 """Debugging helper"""
104 for x
in self
.__dict
__:
105 if x
not in ('env', 'bld', 'compiled_tasks', 'tasks'):
106 lst
.append("%s=%s" % (x
, repr(getattr(self
, x
))))
107 return "bld(%s) in %s" % (", ".join(lst
), self
.path
.abspath())
111 Current working directory for the task generator, defaults to the build directory.
112 This is still used in a few places but it should disappear at some point as the classes
113 define their own working directory.
115 :rtype: :py:class:`waflib.Node.Node`
117 return self
.bld
.bldnode
121 If the attribute ``name`` is not set on the instance,
122 the name is computed from the target name::
127 y = bld(target='bar')
131 :return: name of this task generator
135 except AttributeError:
136 if isinstance(self
.target
, list):
137 lst
= [str(x
) for x
in self
.target
]
138 name
= self
._name
= ','.join(lst
)
140 name
= self
._name
= str(self
.target
)
142 def set_name(self
, name
):
145 name
= property(get_name
, set_name
)
147 def to_list(self
, val
):
149 Ensures that a parameter is a list, see :py:func:`waflib.Utils.to_list`
151 :type val: string or list of string
152 :param val: input to return as a list
155 if isinstance(val
, str):
162 Creates tasks for this task generators. The following operations are performed:
164 #. The body of this method is called only once and sets the attribute ``posted``
165 #. The attribute ``features`` is used to add more methods in ``self.meths``
166 #. The methods are sorted by the precedence table ``self.prec`` or `:waflib:attr:waflib.TaskGen.task_gen.prec`
167 #. The methods are then executed in order
168 #. The tasks created are added to :py:attr:`waflib.TaskGen.task_gen.tasks`
170 if getattr(self
, 'posted', None):
174 keys
= set(self
.meths
)
175 keys
.update(feats
['*'])
177 # add the methods listed in the features
178 self
.features
= Utils
.to_list(self
.features
)
179 for x
in self
.features
:
183 elif not x
in Task
.classes
:
184 Logs
.warn('feature %r does not exist - bind at least one method to it?', x
)
186 # copy the precedence table
191 prec
[x
] = prec_tbl
[x
]
193 # elements disconnected
196 for x
in prec
.values():
202 tmp
.sort(reverse
=True)
222 tmp
.sort(reverse
=True)
225 buf
= ['Cycle detected in the method execution:']
226 for k
, v
in prec
.items():
227 buf
.append('- %s after %s' % (k
, [x
for x
in v
if x
in prec
]))
228 raise Errors
.WafError('\n'.join(buf
))
231 # then we run the methods in order
232 Logs
.debug('task_gen: posting %s %d', self
, id(self
))
236 except AttributeError:
237 raise Errors
.WafError('%r is not a valid task generator method' % x
)
238 Logs
.debug('task_gen: -> %s (%d)', x
, id(self
))
241 Logs
.debug('task_gen: posted %s', self
.name
)
244 def get_hook(self
, node
):
246 Returns the ``@extension`` method to call for a Node of a particular extension.
248 :param node: Input file to process
249 :type node: :py:class:`waflib.Tools.Node.Node`
250 :return: A method able to process the input node by looking at the extension
254 for k
in self
.mappings
:
257 return self
.mappings
[k
]
261 return self
.mappings
[k
]
262 keys
= list(self
.mappings
.keys())
263 raise Errors
.WafError("File %r has no mapping in %r (load a waf tool?)" % (node
, keys
))
265 def create_task(self
, name
, src
=None, tgt
=None, **kw
):
267 Creates task instances.
269 :param name: task class name
271 :param src: input nodes
272 :type src: list of :py:class:`waflib.Tools.Node.Node`
273 :param tgt: output nodes
274 :type tgt: list of :py:class:`waflib.Tools.Node.Node`
275 :return: A task object
276 :rtype: :py:class:`waflib.Task.Task`
278 task
= Task
.classes
[name
](env
=self
.env
.derive(), generator
=self
)
282 task
.set_outputs(tgt
)
283 task
.__dict
__.update(kw
)
284 self
.tasks
.append(task
)
287 def clone(self
, env
):
289 Makes a copy of a task generator. Once the copy is made, it is necessary to ensure that the
290 it does not create the same output files as the original, or the same files may
291 be compiled several times.
293 :param env: A configuration set
294 :type env: :py:class:`waflib.ConfigSet.ConfigSet`
296 :rtype: :py:class:`waflib.TaskGen.task_gen`
299 for x
in self
.__dict
__:
300 if x
in ('env', 'bld'):
302 elif x
in ('path', 'features'):
303 setattr(newobj
, x
, getattr(self
, x
))
305 setattr(newobj
, x
, copy
.copy(getattr(self
, x
)))
307 newobj
.posted
= False
308 if isinstance(env
, str):
309 newobj
.env
= self
.bld
.all_envs
[env
].derive()
311 newobj
.env
= env
.derive()
315 def declare_chain(name
='', rule
=None, reentrant
=None, color
='BLUE',
316 ext_in
=[], ext_out
=[], before
=[], after
=[], decider
=None, scan
=None, install_path
=None, shell
=False):
318 Creates a new mapping and a task class for processing files by extension.
319 See Tools/flex.py for an example.
321 :param name: name for the task class
323 :param rule: function to execute or string to be compiled in a function
324 :type rule: string or function
325 :param reentrant: re-inject the output file in the process (done automatically, set to 0 to disable)
327 :param color: color for the task output
329 :param ext_in: execute the task only after the files of such extensions are created
330 :type ext_in: list of string
331 :param ext_out: execute the task only before files of such extensions are processed
332 :type ext_out: list of string
333 :param before: execute instances of this task before classes of the given names
334 :type before: list of string
335 :param after: execute instances of this task after classes of the given names
336 :type after: list of string
337 :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)
338 :type decider: function
339 :param scan: scanner function for the task
341 :param install_path: installation path for the output nodes
342 :type install_path: string
344 ext_in
= Utils
.to_list(ext_in
)
345 ext_out
= Utils
.to_list(ext_out
)
348 cls
= Task
.task_factory(name
, rule
, color
=color
, ext_in
=ext_in
, ext_out
=ext_out
, before
=before
, after
=after
, scan
=scan
, shell
=shell
)
350 def x_file(self
, node
):
354 tsk
= self
.create_task(name
, node
)
357 ext
= decider(self
, node
) if decider
else cls
.ext_out
359 k
= node
.change_ext(x
, ext_in
=_ext_in
)
360 tsk
.outputs
.append(k
)
362 if reentrant
!= None:
363 if cnt
< int(reentrant
):
364 self
.source
.append(k
)
366 # reinject downstream files into the build
367 for y
in self
.mappings
: # ~ nfile * nextensions :-/
368 if k
.name
.endswith(y
):
369 self
.source
.append(k
)
374 self
.install_task
= self
.add_install_files(install_to
=install_path
, install_from
=tsk
.outputs
)
378 task_gen
.mappings
[x
] = x_file
381 def taskgen_method(func
):
383 Decorator that registers method as a task generator method.
384 The function must accept a task generator as first parameter::
386 from waflib.TaskGen import taskgen_method
391 :param func: task generator method to add
395 setattr(task_gen
, func
.__name
__, func
)
400 Decorator that registers a task generator method that will be executed when the
401 object attribute ``feature`` contains the corresponding key(s)::
403 from waflib.TaskGen import feature
404 @feature('myfeature')
405 def myfunction(self):
406 print('that is my feature!')
408 bld(features='myfeature')
410 :param k: feature names
411 :type k: list of string
414 setattr(task_gen
, func
.__name
__, func
)
416 feats
[name
].update([func
.__name
__])
420 def before_method(*k
):
422 Decorator that registera task generator method which will be executed
423 before the functions of given name(s)::
425 from waflib.TaskGen import feature, before
426 @feature('myfeature')
427 @before_method('fun2')
430 @feature('myfeature')
434 bld(features='myfeature')
436 :param k: method names
437 :type k: list of string
440 setattr(task_gen
, func
.__name
__, func
)
442 task_gen
.prec
[func
.__name
__].add(fun_name
)
445 before
= before_method
447 def after_method(*k
):
449 Decorator that registers a task generator method which will be executed
450 after the functions of given name(s)::
452 from waflib.TaskGen import feature, after
453 @feature('myfeature')
454 @after_method('fun2')
457 @feature('myfeature')
461 bld(features='myfeature')
463 :param k: method names
464 :type k: list of string
467 setattr(task_gen
, func
.__name
__, func
)
469 task_gen
.prec
[fun_name
].add(func
.__name
__)
476 Decorator that registers a task generator method which will be invoked during
477 the processing of source files for the extension given::
479 from waflib import Task
481 run_str = 'cp ${SRC} ${TGT}'
483 def create_maa_file(self, node):
484 self.create_task('mytask', node, node.change_ext('.maa'))
486 bld(source='foo.moo')
489 setattr(task_gen
, func
.__name
__, func
)
491 task_gen
.mappings
[x
] = func
496 def to_nodes(self
, lst
, path
=None):
498 Flatten the input list of string/nodes/lists into a list of nodes.
500 It is used by :py:func:`waflib.TaskGen.process_source` and :py:func:`waflib.TaskGen.process_rule`.
501 It is designed for source files, for folders, see :py:func:`waflib.Tools.ccroot.to_incnodes`:
503 :param lst: input list
504 :type lst: list of string and nodes
505 :param path: path from which to search the nodes (by default, :py:attr:`waflib.TaskGen.task_gen.path`)
506 :type path: :py:class:`waflib.Tools.Node.Node`
507 :rtype: list of :py:class:`waflib.Tools.Node.Node`
510 path
= path
or self
.path
511 find
= path
.find_resource
513 if isinstance(lst
, Node
.Node
):
516 for x
in Utils
.to_list(lst
):
517 if isinstance(x
, str):
519 elif hasattr(x
, 'name'):
522 tmp
.extend(self
.to_nodes(x
))
525 raise Errors
.WafError('source not found: %r in %r' % (x
, self
))
530 def process_source(self
):
532 Processes each element in the attribute ``source`` by extension.
534 #. The *source* list is converted through :py:meth:`waflib.TaskGen.to_nodes` to a list of :py:class:`waflib.Node.Node` first.
535 #. File extensions are mapped to methods having the signature: ``def meth(self, node)`` by :py:meth:`waflib.TaskGen.extension`
536 #. The method is retrieved through :py:meth:`waflib.TaskGen.task_gen.get_hook`
537 #. When called, the methods may modify self.source to append more source to process
538 #. The mappings can map an extension or a filename (see the code below)
540 self
.source
= self
.to_nodes(getattr(self
, 'source', []))
541 for node
in self
.source
:
542 self
.get_hook(node
)(self
, node
)
545 @before_method('process_source')
546 def process_rule(self
):
548 Processes the attribute ``rule``. When present, :py:meth:`waflib.TaskGen.process_source` is disabled::
551 bld(rule='cp ${SRC} ${TGT}', source='wscript', target='bar.txt')
553 Main attributes processed:
555 * rule: command to execute, it can be a tuple of strings for multiple commands
556 * chmod: permissions for the resulting files (integer value such as Utils.O755)
557 * shell: set to False to execute the command directly (default is True to use a shell)
558 * scan: scanner function
559 * vars: list of variables to trigger rebuilds, such as CFLAGS
560 * cls_str: string to display when executing the task
561 * cls_keyword: label to display when executing the task
562 * cache_rule: by default, try to re-use similar classes, set to False to disable
563 * source: list of Node or string objects representing the source files required by this task
564 * target: list of Node or string objects representing the files that this task creates
565 * cwd: current working directory (Node or string)
566 * stdout: standard output, set to None to prevent waf from capturing the text
567 * stderr: standard error, set to None to prevent waf from capturing the text
568 * timeout: timeout for command execution (Python 3)
569 * always: whether to always run the command (False by default)
570 * deep_inputs: whether the task must depend on the input file tasks too (False by default)
572 if not getattr(self
, 'rule', None):
575 # create the task class
576 name
= str(getattr(self
, 'name', None) or self
.target
or getattr(self
.rule
, '__name__', self
.rule
))
578 # or we can put the class in a cache for performance reasons
580 cache
= self
.bld
.cache_rule_attr
581 except AttributeError:
582 cache
= self
.bld
.cache_rule_attr
= {}
584 chmod
= getattr(self
, 'chmod', None)
585 shell
= getattr(self
, 'shell', True)
586 color
= getattr(self
, 'color', 'BLUE')
587 scan
= getattr(self
, 'scan', None)
588 _vars
= getattr(self
, 'vars', [])
589 cls_str
= getattr(self
, 'cls_str', None)
590 cls_keyword
= getattr(self
, 'cls_keyword', None)
591 use_cache
= getattr(self
, 'cache_rule', 'True')
592 deep_inputs
= getattr(self
, 'deep_inputs', False)
594 scan_val
= has_deps
= hasattr(self
, 'deps')
598 key
= Utils
.h_list((name
, self
.rule
, chmod
, shell
, color
, cls_str
, cls_keyword
, scan_val
, _vars
, deep_inputs
))
608 if chmod
is not None:
610 for x
in tsk
.outputs
:
611 os
.chmod(x
.abspath(), tsk
.generator
.chmod
)
612 if isinstance(rule
, tuple):
614 rule
.append(chmod_fun
)
617 rule
= (rule
, chmod_fun
)
619 cls
= Task
.task_factory(name
, rule
, _vars
, shell
=shell
, color
=color
)
622 setattr(cls
, '__str__', self
.cls_str
)
625 setattr(cls
, 'keyword', self
.cls_keyword
)
628 Task
.deep_inputs(cls
)
634 deps
= getattr(self
.generator
, 'deps', None)
635 nodes
= self
.generator
.to_nodes(deps
)
642 # now create one instance
643 tsk
= self
.create_task(name
)
645 for x
in ('after', 'before', 'ext_in', 'ext_out'):
646 setattr(tsk
, x
, getattr(self
, x
, []))
648 if hasattr(self
, 'stdout'):
649 tsk
.stdout
= self
.stdout
651 if hasattr(self
, 'stderr'):
652 tsk
.stderr
= self
.stderr
654 if getattr(self
, 'timeout', None):
655 tsk
.timeout
= self
.timeout
657 if getattr(self
, 'always', None):
658 tsk
.always_run
= True
660 if getattr(self
, 'target', None):
661 if isinstance(self
.target
, str):
662 self
.target
= self
.target
.split()
663 if not isinstance(self
.target
, list):
664 self
.target
= [self
.target
]
665 for x
in self
.target
:
666 if isinstance(x
, str):
667 tsk
.outputs
.append(self
.path
.find_or_declare(x
))
669 x
.parent
.mkdir() # if a node was given, create the required folders
670 tsk
.outputs
.append(x
)
671 if getattr(self
, 'install_path', None):
672 self
.install_task
= self
.add_install_files(install_to
=self
.install_path
,
673 install_from
=tsk
.outputs
, chmod
=getattr(self
, 'chmod', Utils
.O644
))
675 if getattr(self
, 'source', None):
676 tsk
.inputs
= self
.to_nodes(self
.source
)
677 # bypass the execution of process_source by setting the source to an empty list
680 if getattr(self
, 'cwd', None):
683 if isinstance(tsk
.run
, functools
.partial
):
684 # Python documentation says: "partial objects defined in classes
685 # behave like static methods and do not transform into bound
686 # methods during instance attribute look-up."
687 tsk
.run
= functools
.partial(tsk
.run
, tsk
)
690 def sequence_order(self
):
692 Adds a strict sequential constraint between the tasks generated by task generators.
693 It works because task generators are posted in order.
694 It will not post objects which belong to other folders.
698 bld(features='javac seq')
699 bld(features='jar seq')
701 To start a new sequence, set the attribute seq_start, for example::
703 obj = bld(features='seq')
706 Note that the method is executed in last position. This is more an
707 example than a widely-used solution.
709 if self
.meths
and self
.meths
[-1] != 'sequence_order':
710 self
.meths
.append('sequence_order')
713 if getattr(self
, 'seq_start', None):
716 # all the tasks previously declared must be run before these
717 if getattr(self
.bld
, 'prev', None):
719 for x
in self
.bld
.prev
.tasks
:
726 re_m4
= re
.compile(r
'@(\w+)@', re
.M
)
728 class subst_pc(Task
.Task
):
730 Creates *.pc* files from *.pc.in*. The task is executed whenever an input variable used
731 in the substitution changes.
734 def force_permissions(self
):
735 "Private for the time being, we will probably refactor this into run_str=[run1,chmod]"
736 if getattr(self
.generator
, 'chmod', None):
737 for x
in self
.outputs
:
738 os
.chmod(x
.abspath(), self
.generator
.chmod
)
741 "Substitutes variables in a .in file"
743 if getattr(self
.generator
, 'is_copy', None):
744 for i
, x
in enumerate(self
.outputs
):
745 x
.write(self
.inputs
[i
].read('rb'), 'wb')
746 stat
= os
.stat(self
.inputs
[i
].abspath()) # Preserve mtime of the copy
747 os
.utime(self
.outputs
[i
].abspath(), (stat
.st_atime
, stat
.st_mtime
))
748 self
.force_permissions()
751 if getattr(self
.generator
, 'fun', None):
752 ret
= self
.generator
.fun(self
)
754 self
.force_permissions()
757 code
= self
.inputs
[0].read(encoding
=getattr(self
.generator
, 'encoding', 'latin-1'))
758 if getattr(self
.generator
, 'subst_fun', None):
759 code
= self
.generator
.subst_fun(self
, code
)
761 self
.outputs
[0].write(code
, encoding
=getattr(self
.generator
, 'encoding', 'latin-1'))
762 self
.force_permissions()
765 # replace all % by %% to prevent errors by % signs
766 code
= code
.replace('%', '%%')
768 # extract the vars foo into lst and replace @foo@ by %(foo)s
774 return "%%(%s)s" % g(1)
776 code
= getattr(self
.generator
, 're_m4', re_m4
).sub(repl
, code
)
779 d
= self
.generator
.dct
780 except AttributeError:
783 tmp
= getattr(self
.generator
, x
, '') or self
.env
[x
] or self
.env
[x
.upper()]
791 self
.outputs
[0].write(code
, encoding
=getattr(self
.generator
, 'encoding', 'latin-1'))
792 self
.generator
.bld
.raw_deps
[self
.uid()] = lst
794 # make sure the signature is updated
796 delattr(self
, 'cache_sig')
797 except AttributeError:
800 self
.force_permissions()
804 Compute a hash (signature) of the variables used in the substitution
806 bld
= self
.generator
.bld
810 if getattr(self
.generator
, 'fun', None):
811 upd(Utils
.h_fun(self
.generator
.fun
).encode())
812 if getattr(self
.generator
, 'subst_fun', None):
813 upd(Utils
.h_fun(self
.generator
.subst_fun
).encode())
815 # raw_deps: persistent custom values returned by the scanner
816 vars = self
.generator
.bld
.raw_deps
.get(self
.uid(), [])
818 # hash both env vars and task generator attributes
819 act_sig
= bld
.hash_env_vars(env
, vars)
822 lst
= [getattr(self
.generator
, x
, '') for x
in vars]
823 upd(Utils
.h_list(lst
))
825 return self
.m
.digest()
828 def add_pcfile(self
, node
):
830 Processes *.pc.in* files to *.pc*. Installs the results to ``${PREFIX}/lib/pkgconfig/`` by default
833 bld(source='foo.pc.in', install_path='${LIBDIR}/pkgconfig/')
835 tsk
= self
.create_task('subst_pc', node
, node
.change_ext('.pc', '.pc.in'))
836 self
.install_task
= self
.add_install_files(
837 install_to
=getattr(self
, 'install_path', '${LIBDIR}/pkgconfig/'), install_from
=tsk
.outputs
)
839 class subst(subst_pc
):
843 @before_method('process_source', 'process_rule')
844 def process_subst(self
):
846 Defines a transformation that substitutes the contents of *source* files to *target* files::
853 install_path='${LIBDIR}/pkgconfig',
857 The input files are supposed to contain macros of the form *@VAR@*, where *VAR* is an argument
858 of the task generator object.
860 This method overrides the processing by :py:meth:`waflib.TaskGen.process_source`.
863 src
= Utils
.to_list(getattr(self
, 'source', []))
864 if isinstance(src
, Node
.Node
):
866 tgt
= Utils
.to_list(getattr(self
, 'target', []))
867 if isinstance(tgt
, Node
.Node
):
869 if len(src
) != len(tgt
):
870 raise Errors
.WafError('invalid number of source/target for %r' % self
)
872 for x
, y
in zip(src
, tgt
):
874 raise Errors
.WafError('null source or target for %r' % self
)
877 if isinstance(x
, str) and isinstance(y
, str) and x
== y
:
878 a
= self
.path
.find_node(x
)
879 b
= self
.path
.get_bld().make_node(y
)
880 if not os
.path
.isfile(b
.abspath()):
883 if isinstance(x
, str):
884 a
= self
.path
.find_resource(x
)
885 elif isinstance(x
, Node
.Node
):
887 if isinstance(y
, str):
888 b
= self
.path
.find_or_declare(y
)
889 elif isinstance(y
, Node
.Node
):
893 raise Errors
.WafError('could not find %r for %r' % (x
, self
))
895 tsk
= self
.create_task('subst', a
, b
)
896 for k
in ('after', 'before', 'ext_in', 'ext_out'):
897 val
= getattr(self
, k
, None)
901 # paranoid safety measure for the general case foo.in->foo.h with ambiguous dependencies
902 for xt
in HEADER_EXTS
:
903 if b
.name
.endswith(xt
):
904 tsk
.ext_out
= tsk
.ext_out
+ ['.h']
907 inst_to
= getattr(self
, 'install_path', None)
909 self
.install_task
= self
.add_install_files(install_to
=inst_to
,
910 install_from
=b
, chmod
=getattr(self
, 'chmod', Utils
.O644
))