1#!/usr/bin/env python
2# encoding: utf-8
3# Thomas Nagy, 2005-2018 (ita)
4
5"""
6Task generators
7
8The class :py:class:`waflib.TaskGen.task_gen` encapsulates the creation of task objects (low-level code)
9The instances can have various parameters, but the creation of task nodes (Task.py)
10is deferred. To achieve this, various methods are called from the method "apply"
11"""
12
13import copy, re, os, functools
14from waflib import Task, Utils, Logs, Errors, ConfigSet, Node
15
16feats = Utils.defaultdict(set)
17"""remember the methods declaring features"""
18
19HEADER_EXTS = ['.h', '.hpp', '.hxx', '.hh']
20
21class task_gen(object):
22	"""
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.
25	A few notes:
26
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
32	"""
33
34	mappings = Utils.ordered_iter_dict()
35	"""Mappings are global file extension mappings that are retrieved in the order of definition"""
36
37	prec = Utils.defaultdict(set)
38	"""Dict that holds the precedence execution rules for task generator methods"""
39
40	def __init__(self, *k, **kw):
41		"""
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)
44
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.
48
49		The extra key/value elements passed in ``kw`` are set as attributes
50		"""
51		self.source = []
52		self.target = ''
53
54		self.meths = []
55		"""
56		List of method names to execute (internal)
57		"""
58
59		self.features = []
60		"""
61		List of feature names for bringing new methods in
62		"""
63
64		self.tasks = []
65		"""
66		Tasks created are added to this list
67		"""
68
69		if not 'bld' in kw:
70			# task generators without a build context :-/
71			self.env = ConfigSet.ConfigSet()
72			self.idx = 0
73			self.path = None
74		else:
75			self.bld = kw['bld']
76			self.env = self.bld.env.derive()
77			self.path = kw.get('path', self.bld.path) # by default, emulate chdir when reading scripts
78
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()
82			try:
83				self.idx = self.bld.idx[path] = self.bld.idx.get(path, 0) + 1
84			except AttributeError:
85				self.bld.idx = {}
86				self.idx = self.bld.idx[path] = 1
87
88			# Record the global task generator count
89			try:
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
93
94		for key, val in kw.items():
95			setattr(self, key, val)
96
97	def __str__(self):
98		"""Debugging helper"""
99		return "<task_gen %r declared in %s>" % (self.name, self.path.abspath())
100
101	def __repr__(self):
102		"""Debugging helper"""
103		lst = []
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())
108
109	def get_cwd(self):
110		"""
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.
114
115		:rtype: :py:class:`waflib.Node.Node`
116		"""
117		return self.bld.bldnode
118
119	def get_name(self):
120		"""
121		If the attribute ``name`` is not set on the instance,
122		the name is computed from the target name::
123
124			def build(bld):
125				x = bld(name='foo')
126				x.get_name() # foo
127				y = bld(target='bar')
128				y.get_name() # bar
129
130		:rtype: string
131		:return: name of this task generator
132		"""
133		try:
134			return self._name
135		except AttributeError:
136			if isinstance(self.target, list):
137				lst = [str(x) for x in self.target]
138				name = self._name = ','.join(lst)
139			else:
140				name = self._name = str(self.target)
141			return name
142	def set_name(self, name):
143		self._name = name
144
145	name = property(get_name, set_name)
146
147	def to_list(self, val):
148		"""
149		Ensures that a parameter is a list, see :py:func:`waflib.Utils.to_list`
150
151		:type val: string or list of string
152		:param val: input to return as a list
153		:rtype: list
154		"""
155		if isinstance(val, str):
156			return val.split()
157		else:
158			return val
159
160	def post(self):
161		"""
162		Creates tasks for this task generators. The following operations are performed:
163
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`
169		"""
170		if getattr(self, 'posted', None):
171			return False
172		self.posted = True
173
174		keys = set(self.meths)
175		keys.update(feats['*'])
176
177		# add the methods listed in the features
178		self.features = Utils.to_list(self.features)
179		for x in self.features:
180			st = feats[x]
181			if st:
182				keys.update(st)
183			elif not x in Task.classes:
184				Logs.warn('feature %r does not exist - bind at least one method to it?', x)
185
186		# copy the precedence table
187		prec = {}
188		prec_tbl = self.prec
189		for x in prec_tbl:
190			if x in keys:
191				prec[x] = prec_tbl[x]
192
193		# elements disconnected
194		tmp = []
195		for a in keys:
196			for x in prec.values():
197				if a in x:
198					break
199			else:
200				tmp.append(a)
201
202		tmp.sort(reverse=True)
203
204		# topological sort
205		out = []
206		while tmp:
207			e = tmp.pop()
208			if e in keys:
209				out.append(e)
210			try:
211				nlst = prec[e]
212			except KeyError:
213				pass
214			else:
215				del prec[e]
216				for x in nlst:
217					for y in prec:
218						if x in prec[y]:
219							break
220					else:
221						tmp.append(x)
222						tmp.sort(reverse=True)
223
224		if prec:
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))
229		self.meths = out
230
231		# then we run the methods in order
232		Logs.debug('task_gen: posting %s %d', self, id(self))
233		for x in out:
234			try:
235				v = getattr(self, x)
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))
239			v()
240
241		Logs.debug('task_gen: posted %s', self.name)
242		return True
243
244	def get_hook(self, node):
245		"""
246		Returns the ``@extension`` method to call for a Node of a particular extension.
247
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
251		:rtype: function
252		"""
253		name = node.name
254		for k in self.mappings:
255			try:
256				if name.endswith(k):
257					return self.mappings[k]
258			except TypeError:
259				# regexps objects
260				if k.match(name):
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))
264
265	def create_task(self, name, src=None, tgt=None, **kw):
266		"""
267		Creates task instances.
268
269		:param name: task class name
270		:type name: string
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`
277		"""
278		task = Task.classes[name](env=self.env.derive(), generator=self)
279		if src:
280			task.set_inputs(src)
281		if tgt:
282			task.set_outputs(tgt)
283		task.__dict__.update(kw)
284		self.tasks.append(task)
285		return task
286
287	def clone(self, env):
288		"""
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.
292
293		:param env: A configuration set
294		:type env: :py:class:`waflib.ConfigSet.ConfigSet`
295		:return: A copy
296		:rtype: :py:class:`waflib.TaskGen.task_gen`
297		"""
298		newobj = self.bld()
299		for x in self.__dict__:
300			if x in ('env', 'bld'):
301				continue
302			elif x in ('path', 'features'):
303				setattr(newobj, x, getattr(self, x))
304			else:
305				setattr(newobj, x, copy.copy(getattr(self, x)))
306
307		newobj.posted = False
308		if isinstance(env, str):
309			newobj.env = self.bld.all_envs[env].derive()
310		else:
311			newobj.env = env.derive()
312
313		return newobj
314
315def 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):
317	"""
318	Creates a new mapping and a task class for processing files by extension.
319	See Tools/flex.py for an example.
320
321	:param name: name for the task class
322	:type name: string
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)
326	:type reentrant: int
327	:param color: color for the task output
328	:type color: string
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
340	:type scan: function
341	:param install_path: installation path for the output nodes
342	:type install_path: string
343	"""
344	ext_in = Utils.to_list(ext_in)
345	ext_out = Utils.to_list(ext_out)
346	if not name:
347		name = rule
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)
349
350	def x_file(self, node):
351		if ext_in:
352			_ext_in = ext_in[0]
353
354		tsk = self.create_task(name, node)
355		cnt = 0
356
357		ext = decider(self, node) if decider else cls.ext_out
358		for x in ext:
359			k = node.change_ext(x, ext_in=_ext_in)
360			tsk.outputs.append(k)
361
362			if reentrant != None:
363				if cnt < int(reentrant):
364					self.source.append(k)
365			else:
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)
370						break
371			cnt += 1
372
373		if install_path:
374			self.install_task = self.add_install_files(install_to=install_path, install_from=tsk.outputs)
375		return tsk
376
377	for x in cls.ext_in:
378		task_gen.mappings[x] = x_file
379	return x_file
380
381def taskgen_method(func):
382	"""
383	Decorator that registers method as a task generator method.
384	The function must accept a task generator as first parameter::
385
386		from waflib.TaskGen import taskgen_method
387		@taskgen_method
388		def mymethod(self):
389			pass
390
391	:param func: task generator method to add
392	:type func: function
393	:rtype: function
394	"""
395	setattr(task_gen, func.__name__, func)
396	return func
397
398def feature(*k):
399	"""
400	Decorator that registers a task generator method that will be executed when the
401	object attribute ``feature`` contains the corresponding key(s)::
402
403		from waflib.Task import feature
404		@feature('myfeature')
405		def myfunction(self):
406			print('that is my feature!')
407		def build(bld):
408			bld(features='myfeature')
409
410	:param k: feature names
411	:type k: list of string
412	"""
413	def deco(func):
414		setattr(task_gen, func.__name__, func)
415		for name in k:
416			feats[name].update([func.__name__])
417		return func
418	return deco
419
420def before_method(*k):
421	"""
422	Decorator that registera task generator method which will be executed
423	before the functions of given name(s)::
424
425		from waflib.TaskGen import feature, before
426		@feature('myfeature')
427		@before_method('fun2')
428		def fun1(self):
429			print('feature 1!')
430		@feature('myfeature')
431		def fun2(self):
432			print('feature 2!')
433		def build(bld):
434			bld(features='myfeature')
435
436	:param k: method names
437	:type k: list of string
438	"""
439	def deco(func):
440		setattr(task_gen, func.__name__, func)
441		for fun_name in k:
442			task_gen.prec[func.__name__].add(fun_name)
443		return func
444	return deco
445before = before_method
446
447def after_method(*k):
448	"""
449	Decorator that registers a task generator method which will be executed
450	after the functions of given name(s)::
451
452		from waflib.TaskGen import feature, after
453		@feature('myfeature')
454		@after_method('fun2')
455		def fun1(self):
456			print('feature 1!')
457		@feature('myfeature')
458		def fun2(self):
459			print('feature 2!')
460		def build(bld):
461			bld(features='myfeature')
462
463	:param k: method names
464	:type k: list of string
465	"""
466	def deco(func):
467		setattr(task_gen, func.__name__, func)
468		for fun_name in k:
469			task_gen.prec[fun_name].add(func.__name__)
470		return func
471	return deco
472after = after_method
473
474def extension(*k):
475	"""
476	Decorator that registers a task generator method which will be invoked during
477	the processing of source files for the extension given::
478
479		from waflib import Task
480		class mytask(Task):
481			run_str = 'cp ${SRC} ${TGT}'
482		@extension('.moo')
483		def create_maa_file(self, node):
484			self.create_task('mytask', node, node.change_ext('.maa'))
485		def build(bld):
486			bld(source='foo.moo')
487	"""
488	def deco(func):
489		setattr(task_gen, func.__name__, func)
490		for x in k:
491			task_gen.mappings[x] = func
492		return func
493	return deco
494
495@taskgen_method
496def to_nodes(self, lst, path=None):
497	"""
498	Flatten the input list of string/nodes/lists into a list of nodes.
499
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`:
502
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`
508	"""
509	tmp = []
510	path = path or self.path
511	find = path.find_resource
512
513	if isinstance(lst, Node.Node):
514		lst = [lst]
515
516	for x in Utils.to_list(lst):
517		if isinstance(x, str):
518			node = find(x)
519		elif hasattr(x, 'name'):
520			node = x
521		else:
522			tmp.extend(self.to_nodes(x))
523			continue
524		if not node:
525			raise Errors.WafError('source not found: %r in %r' % (x, self))
526		tmp.append(node)
527	return tmp
528
529@feature('*')
530def process_source(self):
531	"""
532	Processes each element in the attribute ``source`` by extension.
533
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)
539	"""
540	self.source = self.to_nodes(getattr(self, 'source', []))
541	for node in self.source:
542		self.get_hook(node)(self, node)
543
544@feature('*')
545@before_method('process_source')
546def process_rule(self):
547	"""
548	Processes the attribute ``rule``. When present, :py:meth:`waflib.TaskGen.process_source` is disabled::
549
550		def build(bld):
551			bld(rule='cp ${SRC} ${TGT}', source='wscript', target='bar.txt')
552
553	Main attributes processed:
554
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)
571	"""
572	if not getattr(self, 'rule', None):
573		return
574
575	# create the task class
576	name = str(getattr(self, 'name', None) or self.target or getattr(self.rule, '__name__', self.rule))
577
578	# or we can put the class in a cache for performance reasons
579	try:
580		cache = self.bld.cache_rule_attr
581	except AttributeError:
582		cache = self.bld.cache_rule_attr = {}
583
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)
593
594	scan_val = has_deps = hasattr(self, 'deps')
595	if scan:
596		scan_val = id(scan)
597
598	key = Utils.h_list((name, self.rule, chmod, shell, color, cls_str, cls_keyword, scan_val, _vars, deep_inputs))
599
600	cls = None
601	if use_cache:
602		try:
603			cls = cache[key]
604		except KeyError:
605			pass
606	if not cls:
607		rule = self.rule
608		if chmod is not None:
609			def chmod_fun(tsk):
610				for x in tsk.outputs:
611					os.chmod(x.abspath(), tsk.generator.chmod)
612			if isinstance(rule, tuple):
613				rule = list(rule)
614				rule.append(chmod_fun)
615				rule = tuple(rule)
616			else:
617				rule = (rule, chmod_fun)
618
619		cls = Task.task_factory(name, rule, _vars, shell=shell, color=color)
620
621		if cls_str:
622			setattr(cls, '__str__', self.cls_str)
623
624		if cls_keyword:
625			setattr(cls, 'keyword', self.cls_keyword)
626
627		if deep_inputs:
628			Task.deep_inputs(cls)
629
630		if scan:
631			cls.scan = self.scan
632		elif has_deps:
633			def scan(self):
634				nodes = []
635				for x in self.generator.to_list(getattr(self.generator, 'deps', None)):
636					node = self.generator.path.find_resource(x)
637					if not node:
638						self.generator.bld.fatal('Could not find %r (was it declared?)' % x)
639					nodes.append(node)
640				return [nodes, []]
641			cls.scan = scan
642
643		if use_cache:
644			cache[key] = cls
645
646	# now create one instance
647	tsk = self.create_task(name)
648
649	for x in ('after', 'before', 'ext_in', 'ext_out'):
650		setattr(tsk, x, getattr(self, x, []))
651
652	if hasattr(self, 'stdout'):
653		tsk.stdout = self.stdout
654
655	if hasattr(self, 'stderr'):
656		tsk.stderr = self.stderr
657
658	if getattr(self, 'timeout', None):
659		tsk.timeout = self.timeout
660
661	if getattr(self, 'always', None):
662		tsk.always_run = True
663
664	if getattr(self, 'target', None):
665		if isinstance(self.target, str):
666			self.target = self.target.split()
667		if not isinstance(self.target, list):
668			self.target = [self.target]
669		for x in self.target:
670			if isinstance(x, str):
671				tsk.outputs.append(self.path.find_or_declare(x))
672			else:
673				x.parent.mkdir() # if a node was given, create the required folders
674				tsk.outputs.append(x)
675		if getattr(self, 'install_path', None):
676			self.install_task = self.add_install_files(install_to=self.install_path,
677				install_from=tsk.outputs, chmod=getattr(self, 'chmod', Utils.O644))
678
679	if getattr(self, 'source', None):
680		tsk.inputs = self.to_nodes(self.source)
681		# bypass the execution of process_source by setting the source to an empty list
682		self.source = []
683
684	if getattr(self, 'cwd', None):
685		tsk.cwd = self.cwd
686
687	if isinstance(tsk.run, functools.partial):
688		# Python documentation says: "partial objects defined in classes
689		# behave like static methods and do not transform into bound
690		# methods during instance attribute look-up."
691		tsk.run = functools.partial(tsk.run, tsk)
692
693@feature('seq')
694def sequence_order(self):
695	"""
696	Adds a strict sequential constraint between the tasks generated by task generators.
697	It works because task generators are posted in order.
698	It will not post objects which belong to other folders.
699
700	Example::
701
702		bld(features='javac seq')
703		bld(features='jar seq')
704
705	To start a new sequence, set the attribute seq_start, for example::
706
707		obj = bld(features='seq')
708		obj.seq_start = True
709
710	Note that the method is executed in last position. This is more an
711	example than a widely-used solution.
712	"""
713	if self.meths and self.meths[-1] != 'sequence_order':
714		self.meths.append('sequence_order')
715		return
716
717	if getattr(self, 'seq_start', None):
718		return
719
720	# all the tasks previously declared must be run before these
721	if getattr(self.bld, 'prev', None):
722		self.bld.prev.post()
723		for x in self.bld.prev.tasks:
724			for y in self.tasks:
725				y.set_run_after(x)
726
727	self.bld.prev = self
728
729
730re_m4 = re.compile(r'@(\w+)@', re.M)
731
732class subst_pc(Task.Task):
733	"""
734	Creates *.pc* files from *.pc.in*. The task is executed whenever an input variable used
735	in the substitution changes.
736	"""
737
738	def force_permissions(self):
739		"Private for the time being, we will probably refactor this into run_str=[run1,chmod]"
740		if getattr(self.generator, 'chmod', None):
741			for x in self.outputs:
742				os.chmod(x.abspath(), self.generator.chmod)
743
744	def run(self):
745		"Substitutes variables in a .in file"
746
747		if getattr(self.generator, 'is_copy', None):
748			for i, x in enumerate(self.outputs):
749				x.write(self.inputs[i].read('rb'), 'wb')
750				stat = os.stat(self.inputs[i].abspath()) # Preserve mtime of the copy
751				os.utime(self.outputs[i].abspath(), (stat.st_atime, stat.st_mtime))
752			self.force_permissions()
753			return None
754
755		if getattr(self.generator, 'fun', None):
756			ret = self.generator.fun(self)
757			if not ret:
758				self.force_permissions()
759			return ret
760
761		code = self.inputs[0].read(encoding=getattr(self.generator, 'encoding', 'latin-1'))
762		if getattr(self.generator, 'subst_fun', None):
763			code = self.generator.subst_fun(self, code)
764			if code is not None:
765				self.outputs[0].write(code, encoding=getattr(self.generator, 'encoding', 'latin-1'))
766			self.force_permissions()
767			return None
768
769		# replace all % by %% to prevent errors by % signs
770		code = code.replace('%', '%%')
771
772		# extract the vars foo into lst and replace @foo@ by %(foo)s
773		lst = []
774		def repl(match):
775			g = match.group
776			if g(1):
777				lst.append(g(1))
778				return "%%(%s)s" % g(1)
779			return ''
780		code = getattr(self.generator, 're_m4', re_m4).sub(repl, code)
781
782		try:
783			d = self.generator.dct
784		except AttributeError:
785			d = {}
786			for x in lst:
787				tmp = getattr(self.generator, x, '') or self.env[x] or self.env[x.upper()]
788				try:
789					tmp = ''.join(tmp)
790				except TypeError:
791					tmp = str(tmp)
792				d[x] = tmp
793
794		code = code % d
795		self.outputs[0].write(code, encoding=getattr(self.generator, 'encoding', 'latin-1'))
796		self.generator.bld.raw_deps[self.uid()] = lst
797
798		# make sure the signature is updated
799		try:
800			delattr(self, 'cache_sig')
801		except AttributeError:
802			pass
803
804		self.force_permissions()
805
806	def sig_vars(self):
807		"""
808		Compute a hash (signature) of the variables used in the substitution
809		"""
810		bld = self.generator.bld
811		env = self.env
812		upd = self.m.update
813
814		if getattr(self.generator, 'fun', None):
815			upd(Utils.h_fun(self.generator.fun).encode())
816		if getattr(self.generator, 'subst_fun', None):
817			upd(Utils.h_fun(self.generator.subst_fun).encode())
818
819		# raw_deps: persistent custom values returned by the scanner
820		vars = self.generator.bld.raw_deps.get(self.uid(), [])
821
822		# hash both env vars and task generator attributes
823		act_sig = bld.hash_env_vars(env, vars)
824		upd(act_sig)
825
826		lst = [getattr(self.generator, x, '') for x in vars]
827		upd(Utils.h_list(lst))
828
829		return self.m.digest()
830
831@extension('.pc.in')
832def add_pcfile(self, node):
833	"""
834	Processes *.pc.in* files to *.pc*. Installs the results to ``${PREFIX}/lib/pkgconfig/`` by default
835
836		def build(bld):
837			bld(source='foo.pc.in', install_path='${LIBDIR}/pkgconfig/')
838	"""
839	tsk = self.create_task('subst_pc', node, node.change_ext('.pc', '.pc.in'))
840	self.install_task = self.add_install_files(
841		install_to=getattr(self, 'install_path', '${LIBDIR}/pkgconfig/'), install_from=tsk.outputs)
842
843class subst(subst_pc):
844	pass
845
846@feature('subst')
847@before_method('process_source', 'process_rule')
848def process_subst(self):
849	"""
850	Defines a transformation that substitutes the contents of *source* files to *target* files::
851
852		def build(bld):
853			bld(
854				features='subst',
855				source='foo.c.in',
856				target='foo.c',
857				install_path='${LIBDIR}/pkgconfig',
858				VAR = 'val'
859			)
860
861	The input files are supposed to contain macros of the form *@VAR@*, where *VAR* is an argument
862	of the task generator object.
863
864	This method overrides the processing by :py:meth:`waflib.TaskGen.process_source`.
865	"""
866
867	src = Utils.to_list(getattr(self, 'source', []))
868	if isinstance(src, Node.Node):
869		src = [src]
870	tgt = Utils.to_list(getattr(self, 'target', []))
871	if isinstance(tgt, Node.Node):
872		tgt = [tgt]
873	if len(src) != len(tgt):
874		raise Errors.WafError('invalid number of source/target for %r' % self)
875
876	for x, y in zip(src, tgt):
877		if not x or not y:
878			raise Errors.WafError('null source or target for %r' % self)
879		a, b = None, None
880
881		if isinstance(x, str) and isinstance(y, str) and x == y:
882			a = self.path.find_node(x)
883			b = self.path.get_bld().make_node(y)
884			if not os.path.isfile(b.abspath()):
885				b.parent.mkdir()
886		else:
887			if isinstance(x, str):
888				a = self.path.find_resource(x)
889			elif isinstance(x, Node.Node):
890				a = x
891			if isinstance(y, str):
892				b = self.path.find_or_declare(y)
893			elif isinstance(y, Node.Node):
894				b = y
895
896		if not a:
897			raise Errors.WafError('could not find %r for %r' % (x, self))
898
899		tsk = self.create_task('subst', a, b)
900		for k in ('after', 'before', 'ext_in', 'ext_out'):
901			val = getattr(self, k, None)
902			if val:
903				setattr(tsk, k, val)
904
905		# paranoid safety measure for the general case foo.in->foo.h with ambiguous dependencies
906		for xt in HEADER_EXTS:
907			if b.name.endswith(xt):
908				tsk.ext_out = tsk.ext_out + ['.h']
909				break
910
911		inst_to = getattr(self, 'install_path', None)
912		if inst_to:
913			self.install_task = self.add_install_files(install_to=inst_to,
914				install_from=b, chmod=getattr(self, 'chmod', Utils.O644))
915
916	self.source = []
917
918