1#!/usr/bin/env python
2# encoding: utf-8
3# Thomas Nagy, 2010-2018 (ita)
4
5"""
6Classes and functions enabling the command system
7"""
8
9import os, re, imp, sys
10from waflib import Utils, Errors, Logs
11import waflib.Node
12
13# the following 3 constants are updated on each new release (do not touch)
14HEXVERSION=0x2001200
15"""Constant updated on new releases"""
16
17WAFVERSION="2.0.18"
18"""Constant updated on new releases"""
19
20WAFREVISION="314689b8994259a84f0de0aaef74d7ce91f541ad"
21"""Git revision when the waf version is updated"""
22
23ABI = 20
24"""Version of the build data cache file format (used in :py:const:`waflib.Context.DBFILE`)"""
25
26DBFILE = '.wafpickle-%s-%d-%d' % (sys.platform, sys.hexversion, ABI)
27"""Name of the pickle file for storing the build data"""
28
29APPNAME = 'APPNAME'
30"""Default application name (used by ``waf dist``)"""
31
32VERSION = 'VERSION'
33"""Default application version (used by ``waf dist``)"""
34
35TOP  = 'top'
36"""The variable name for the top-level directory in wscript files"""
37
38OUT  = 'out'
39"""The variable name for the output directory in wscript files"""
40
41WSCRIPT_FILE = 'wscript'
42"""Name of the waf script files"""
43
44launch_dir = ''
45"""Directory from which waf has been called"""
46run_dir = ''
47"""Location of the wscript file to use as the entry point"""
48top_dir = ''
49"""Location of the project directory (top), if the project was configured"""
50out_dir = ''
51"""Location of the build directory (out), if the project was configured"""
52waf_dir = ''
53"""Directory containing the waf modules"""
54
55default_encoding = Utils.console_encoding()
56"""Encoding to use when reading outputs from other processes"""
57
58g_module = None
59"""
60Module representing the top-level wscript file (see :py:const:`waflib.Context.run_dir`)
61"""
62
63STDOUT = 1
64STDERR = -1
65BOTH   = 0
66
67classes = []
68"""
69List of :py:class:`waflib.Context.Context` subclasses that can be used as waf commands. The classes
70are added automatically by a metaclass.
71"""
72
73def create_context(cmd_name, *k, **kw):
74	"""
75	Returns a new :py:class:`waflib.Context.Context` instance corresponding to the given command.
76	Used in particular by :py:func:`waflib.Scripting.run_command`
77
78	:param cmd_name: command name
79	:type cmd_name: string
80	:param k: arguments to give to the context class initializer
81	:type k: list
82	:param k: keyword arguments to give to the context class initializer
83	:type k: dict
84	:return: Context object
85	:rtype: :py:class:`waflib.Context.Context`
86	"""
87	for x in classes:
88		if x.cmd == cmd_name:
89			return x(*k, **kw)
90	ctx = Context(*k, **kw)
91	ctx.fun = cmd_name
92	return ctx
93
94class store_context(type):
95	"""
96	Metaclass that registers command classes into the list :py:const:`waflib.Context.classes`
97	Context classes must provide an attribute 'cmd' representing the command name, and a function
98	attribute 'fun' representing the function name that the command uses.
99	"""
100	def __init__(cls, name, bases, dct):
101		super(store_context, cls).__init__(name, bases, dct)
102		name = cls.__name__
103
104		if name in ('ctx', 'Context'):
105			return
106
107		try:
108			cls.cmd
109		except AttributeError:
110			raise Errors.WafError('Missing command for the context class %r (cmd)' % name)
111
112		if not getattr(cls, 'fun', None):
113			cls.fun = cls.cmd
114
115		classes.insert(0, cls)
116
117ctx = store_context('ctx', (object,), {})
118"""Base class for all :py:class:`waflib.Context.Context` classes"""
119
120class Context(ctx):
121	"""
122	Default context for waf commands, and base class for new command contexts.
123
124	Context objects are passed to top-level functions::
125
126		def foo(ctx):
127			print(ctx.__class__.__name__) # waflib.Context.Context
128
129	Subclasses must define the class attributes 'cmd' and 'fun':
130
131	:param cmd: command to execute as in ``waf cmd``
132	:type cmd: string
133	:param fun: function name to execute when the command is called
134	:type fun: string
135
136	.. inheritance-diagram:: waflib.Context.Context waflib.Build.BuildContext waflib.Build.InstallContext waflib.Build.UninstallContext waflib.Build.StepContext waflib.Build.ListContext waflib.Configure.ConfigurationContext waflib.Scripting.Dist waflib.Scripting.DistCheck waflib.Build.CleanContext
137
138	"""
139
140	errors = Errors
141	"""
142	Shortcut to :py:mod:`waflib.Errors` provided for convenience
143	"""
144
145	tools = {}
146	"""
147	A module cache for wscript files; see :py:meth:`Context.Context.load`
148	"""
149
150	def __init__(self, **kw):
151		try:
152			rd = kw['run_dir']
153		except KeyError:
154			rd = run_dir
155
156		# binds the context to the nodes in use to avoid a context singleton
157		self.node_class = type('Nod3', (waflib.Node.Node,), {})
158		self.node_class.__module__ = 'waflib.Node'
159		self.node_class.ctx = self
160
161		self.root = self.node_class('', None)
162		self.cur_script = None
163		self.path = self.root.find_dir(rd)
164
165		self.stack_path = []
166		self.exec_dict = {'ctx':self, 'conf':self, 'bld':self, 'opt':self}
167		self.logger = None
168
169	def finalize(self):
170		"""
171		Called to free resources such as logger files
172		"""
173		try:
174			logger = self.logger
175		except AttributeError:
176			pass
177		else:
178			Logs.free_logger(logger)
179			delattr(self, 'logger')
180
181	def load(self, tool_list, *k, **kw):
182		"""
183		Loads a Waf tool as a module, and try calling the function named :py:const:`waflib.Context.Context.fun`
184		from it.  A ``tooldir`` argument may be provided as a list of module paths.
185
186		:param tool_list: list of Waf tool names to load
187		:type tool_list: list of string or space-separated string
188		"""
189		tools = Utils.to_list(tool_list)
190		path = Utils.to_list(kw.get('tooldir', ''))
191		with_sys_path = kw.get('with_sys_path', True)
192
193		for t in tools:
194			module = load_tool(t, path, with_sys_path=with_sys_path)
195			fun = getattr(module, kw.get('name', self.fun), None)
196			if fun:
197				fun(self)
198
199	def execute(self):
200		"""
201		Here, it calls the function name in the top-level wscript file. Most subclasses
202		redefine this method to provide additional functionality.
203		"""
204		self.recurse([os.path.dirname(g_module.root_path)])
205
206	def pre_recurse(self, node):
207		"""
208		Method executed immediately before a folder is read by :py:meth:`waflib.Context.Context.recurse`.
209		The current script is bound as a Node object on ``self.cur_script``, and the current path
210		is bound to ``self.path``
211
212		:param node: script
213		:type node: :py:class:`waflib.Node.Node`
214		"""
215		self.stack_path.append(self.cur_script)
216
217		self.cur_script = node
218		self.path = node.parent
219
220	def post_recurse(self, node):
221		"""
222		Restores ``self.cur_script`` and ``self.path`` right after :py:meth:`waflib.Context.Context.recurse` terminates.
223
224		:param node: script
225		:type node: :py:class:`waflib.Node.Node`
226		"""
227		self.cur_script = self.stack_path.pop()
228		if self.cur_script:
229			self.path = self.cur_script.parent
230
231	def recurse(self, dirs, name=None, mandatory=True, once=True, encoding=None):
232		"""
233		Runs user-provided functions from the supplied list of directories.
234		The directories can be either absolute, or relative to the directory
235		of the wscript file
236
237		The methods :py:meth:`waflib.Context.Context.pre_recurse` and
238		:py:meth:`waflib.Context.Context.post_recurse` are called immediately before
239		and after a script has been executed.
240
241		:param dirs: List of directories to visit
242		:type dirs: list of string or space-separated string
243		:param name: Name of function to invoke from the wscript
244		:type  name: string
245		:param mandatory: whether sub wscript files are required to exist
246		:type  mandatory: bool
247		:param once: read the script file once for a particular context
248		:type once: bool
249		"""
250		try:
251			cache = self.recurse_cache
252		except AttributeError:
253			cache = self.recurse_cache = {}
254
255		for d in Utils.to_list(dirs):
256
257			if not os.path.isabs(d):
258				# absolute paths only
259				d = os.path.join(self.path.abspath(), d)
260
261			WSCRIPT     = os.path.join(d, WSCRIPT_FILE)
262			WSCRIPT_FUN = WSCRIPT + '_' + (name or self.fun)
263
264			node = self.root.find_node(WSCRIPT_FUN)
265			if node and (not once or node not in cache):
266				cache[node] = True
267				self.pre_recurse(node)
268				try:
269					function_code = node.read('r', encoding)
270					exec(compile(function_code, node.abspath(), 'exec'), self.exec_dict)
271				finally:
272					self.post_recurse(node)
273			elif not node:
274				node = self.root.find_node(WSCRIPT)
275				tup = (node, name or self.fun)
276				if node and (not once or tup not in cache):
277					cache[tup] = True
278					self.pre_recurse(node)
279					try:
280						wscript_module = load_module(node.abspath(), encoding=encoding)
281						user_function = getattr(wscript_module, (name or self.fun), None)
282						if not user_function:
283							if not mandatory:
284								continue
285							raise Errors.WafError('No function %r defined in %s' % (name or self.fun, node.abspath()))
286						user_function(self)
287					finally:
288						self.post_recurse(node)
289				elif not node:
290					if not mandatory:
291						continue
292					try:
293						os.listdir(d)
294					except OSError:
295						raise Errors.WafError('Cannot read the folder %r' % d)
296					raise Errors.WafError('No wscript file in directory %s' % d)
297
298	def log_command(self, cmd, kw):
299		if Logs.verbose:
300			fmt = os.environ.get('WAF_CMD_FORMAT')
301			if fmt == 'string':
302				if not isinstance(cmd, str):
303					cmd = Utils.shell_escape(cmd)
304			Logs.debug('runner: %r', cmd)
305			Logs.debug('runner_env: kw=%s', kw)
306
307	def exec_command(self, cmd, **kw):
308		"""
309		Runs an external process and returns the exit status::
310
311			def run(tsk):
312				ret = tsk.generator.bld.exec_command('touch foo.txt')
313				return ret
314
315		If the context has the attribute 'log', then captures and logs the process stderr/stdout.
316		Unlike :py:meth:`waflib.Context.Context.cmd_and_log`, this method does not return the
317		stdout/stderr values captured.
318
319		:param cmd: command argument for subprocess.Popen
320		:type cmd: string or list
321		:param kw: keyword arguments for subprocess.Popen. The parameters input/timeout will be passed to wait/communicate.
322		:type kw: dict
323		:returns: process exit status
324		:rtype: integer
325		:raises: :py:class:`waflib.Errors.WafError` if an invalid executable is specified for a non-shell process
326		:raises: :py:class:`waflib.Errors.WafError` in case of execution failure
327		"""
328		subprocess = Utils.subprocess
329		kw['shell'] = isinstance(cmd, str)
330		self.log_command(cmd, kw)
331
332		if self.logger:
333			self.logger.info(cmd)
334
335		if 'stdout' not in kw:
336			kw['stdout'] = subprocess.PIPE
337		if 'stderr' not in kw:
338			kw['stderr'] = subprocess.PIPE
339
340		if Logs.verbose and not kw['shell'] and not Utils.check_exe(cmd[0]):
341			raise Errors.WafError('Program %s not found!' % cmd[0])
342
343		cargs = {}
344		if 'timeout' in kw:
345			if sys.hexversion >= 0x3030000:
346				cargs['timeout'] = kw['timeout']
347				if not 'start_new_session' in kw:
348					kw['start_new_session'] = True
349			del kw['timeout']
350		if 'input' in kw:
351			if kw['input']:
352				cargs['input'] = kw['input']
353				kw['stdin'] = subprocess.PIPE
354			del kw['input']
355
356		if 'cwd' in kw:
357			if not isinstance(kw['cwd'], str):
358				kw['cwd'] = kw['cwd'].abspath()
359
360		encoding = kw.pop('decode_as', default_encoding)
361
362		try:
363			ret, out, err = Utils.run_process(cmd, kw, cargs)
364		except Exception as e:
365			raise Errors.WafError('Execution failure: %s' % str(e), ex=e)
366
367		if out:
368			if not isinstance(out, str):
369				out = out.decode(encoding, errors='replace')
370			if self.logger:
371				self.logger.debug('out: %s', out)
372			else:
373				Logs.info(out, extra={'stream':sys.stdout, 'c1': ''})
374		if err:
375			if not isinstance(err, str):
376				err = err.decode(encoding, errors='replace')
377			if self.logger:
378				self.logger.error('err: %s' % err)
379			else:
380				Logs.info(err, extra={'stream':sys.stderr, 'c1': ''})
381
382		return ret
383
384	def cmd_and_log(self, cmd, **kw):
385		"""
386		Executes a process and returns stdout/stderr if the execution is successful.
387		An exception is thrown when the exit status is non-0. In that case, both stderr and stdout
388		will be bound to the WafError object (configuration tests)::
389
390			def configure(conf):
391				out = conf.cmd_and_log(['echo', 'hello'], output=waflib.Context.STDOUT, quiet=waflib.Context.BOTH)
392				(out, err) = conf.cmd_and_log(['echo', 'hello'], output=waflib.Context.BOTH)
393				(out, err) = conf.cmd_and_log(cmd, input='\\n'.encode(), output=waflib.Context.STDOUT)
394				try:
395					conf.cmd_and_log(['which', 'someapp'], output=waflib.Context.BOTH)
396				except Errors.WafError as e:
397					print(e.stdout, e.stderr)
398
399		:param cmd: args for subprocess.Popen
400		:type cmd: list or string
401		:param kw: keyword arguments for subprocess.Popen. The parameters input/timeout will be passed to wait/communicate.
402		:type kw: dict
403		:returns: a tuple containing the contents of stdout and stderr
404		:rtype: string
405		:raises: :py:class:`waflib.Errors.WafError` if an invalid executable is specified for a non-shell process
406		:raises: :py:class:`waflib.Errors.WafError` in case of execution failure; stdout/stderr/returncode are bound to the exception object
407		"""
408		subprocess = Utils.subprocess
409		kw['shell'] = isinstance(cmd, str)
410		self.log_command(cmd, kw)
411
412		quiet = kw.pop('quiet', None)
413		to_ret = kw.pop('output', STDOUT)
414
415		if Logs.verbose and not kw['shell'] and not Utils.check_exe(cmd[0]):
416			raise Errors.WafError('Program %r not found!' % cmd[0])
417
418		kw['stdout'] = kw['stderr'] = subprocess.PIPE
419		if quiet is None:
420			self.to_log(cmd)
421
422		cargs = {}
423		if 'timeout' in kw:
424			if sys.hexversion >= 0x3030000:
425				cargs['timeout'] = kw['timeout']
426				if not 'start_new_session' in kw:
427					kw['start_new_session'] = True
428			del kw['timeout']
429		if 'input' in kw:
430			if kw['input']:
431				cargs['input'] = kw['input']
432				kw['stdin'] = subprocess.PIPE
433			del kw['input']
434
435		if 'cwd' in kw:
436			if not isinstance(kw['cwd'], str):
437				kw['cwd'] = kw['cwd'].abspath()
438
439		encoding = kw.pop('decode_as', default_encoding)
440
441		try:
442			ret, out, err = Utils.run_process(cmd, kw, cargs)
443		except Exception as e:
444			raise Errors.WafError('Execution failure: %s' % str(e), ex=e)
445
446		if not isinstance(out, str):
447			out = out.decode(encoding, errors='replace')
448		if not isinstance(err, str):
449			err = err.decode(encoding, errors='replace')
450
451		if out and quiet != STDOUT and quiet != BOTH:
452			self.to_log('out: %s' % out)
453		if err and quiet != STDERR and quiet != BOTH:
454			self.to_log('err: %s' % err)
455
456		if ret:
457			e = Errors.WafError('Command %r returned %r' % (cmd, ret))
458			e.returncode = ret
459			e.stderr = err
460			e.stdout = out
461			raise e
462
463		if to_ret == BOTH:
464			return (out, err)
465		elif to_ret == STDERR:
466			return err
467		return out
468
469	def fatal(self, msg, ex=None):
470		"""
471		Prints an error message in red and stops command execution; this is
472		usually used in the configuration section::
473
474			def configure(conf):
475				conf.fatal('a requirement is missing')
476
477		:param msg: message to display
478		:type msg: string
479		:param ex: optional exception object
480		:type ex: exception
481		:raises: :py:class:`waflib.Errors.ConfigurationError`
482		"""
483		if self.logger:
484			self.logger.info('from %s: %s' % (self.path.abspath(), msg))
485		try:
486			logfile = self.logger.handlers[0].baseFilename
487		except AttributeError:
488			pass
489		else:
490			if os.environ.get('WAF_PRINT_FAILURE_LOG'):
491				# see #1930
492				msg = 'Log from (%s):\n%s\n' % (logfile, Utils.readf(logfile))
493			else:
494				msg = '%s\n(complete log in %s)' % (msg, logfile)
495		raise self.errors.ConfigurationError(msg, ex=ex)
496
497	def to_log(self, msg):
498		"""
499		Logs information to the logger (if present), or to stderr.
500		Empty messages are not printed::
501
502			def build(bld):
503				bld.to_log('starting the build')
504
505		Provide a logger on the context class or override this method if necessary.
506
507		:param msg: message
508		:type msg: string
509		"""
510		if not msg:
511			return
512		if self.logger:
513			self.logger.info(msg)
514		else:
515			sys.stderr.write(str(msg))
516			sys.stderr.flush()
517
518
519	def msg(self, *k, **kw):
520		"""
521		Prints a configuration message of the form ``msg: result``.
522		The second part of the message will be in colors. The output
523		can be disabled easly by setting ``in_msg`` to a positive value::
524
525			def configure(conf):
526				self.in_msg = 1
527				conf.msg('Checking for library foo', 'ok')
528				# no output
529
530		:param msg: message to display to the user
531		:type msg: string
532		:param result: result to display
533		:type result: string or boolean
534		:param color: color to use, see :py:const:`waflib.Logs.colors_lst`
535		:type color: string
536		"""
537		try:
538			msg = kw['msg']
539		except KeyError:
540			msg = k[0]
541
542		self.start_msg(msg, **kw)
543
544		try:
545			result = kw['result']
546		except KeyError:
547			result = k[1]
548
549		color = kw.get('color')
550		if not isinstance(color, str):
551			color = result and 'GREEN' or 'YELLOW'
552
553		self.end_msg(result, color, **kw)
554
555	def start_msg(self, *k, **kw):
556		"""
557		Prints the beginning of a 'Checking for xxx' message. See :py:meth:`waflib.Context.Context.msg`
558		"""
559		if kw.get('quiet'):
560			return
561
562		msg = kw.get('msg') or k[0]
563		try:
564			if self.in_msg:
565				self.in_msg += 1
566				return
567		except AttributeError:
568			self.in_msg = 0
569		self.in_msg += 1
570
571		try:
572			self.line_just = max(self.line_just, len(msg))
573		except AttributeError:
574			self.line_just = max(40, len(msg))
575		for x in (self.line_just * '-', msg):
576			self.to_log(x)
577		Logs.pprint('NORMAL', "%s :" % msg.ljust(self.line_just), sep='')
578
579	def end_msg(self, *k, **kw):
580		"""Prints the end of a 'Checking for' message. See :py:meth:`waflib.Context.Context.msg`"""
581		if kw.get('quiet'):
582			return
583		self.in_msg -= 1
584		if self.in_msg:
585			return
586
587		result = kw.get('result') or k[0]
588
589		defcolor = 'GREEN'
590		if result is True:
591			msg = 'ok'
592		elif not result:
593			msg = 'not found'
594			defcolor = 'YELLOW'
595		else:
596			msg = str(result)
597
598		self.to_log(msg)
599		try:
600			color = kw['color']
601		except KeyError:
602			if len(k) > 1 and k[1] in Logs.colors_lst:
603				# compatibility waf 1.7
604				color = k[1]
605			else:
606				color = defcolor
607		Logs.pprint(color, msg)
608
609	def load_special_tools(self, var, ban=[]):
610		"""
611		Loads third-party extensions modules for certain programming languages
612		by trying to list certain files in the extras/ directory. This method
613		is typically called once for a programming language group, see for
614		example :py:mod:`waflib.Tools.compiler_c`
615
616		:param var: glob expression, for example 'cxx\\_\\*.py'
617		:type var: string
618		:param ban: list of exact file names to exclude
619		:type ban: list of string
620		"""
621		if os.path.isdir(waf_dir):
622			lst = self.root.find_node(waf_dir).find_node('waflib/extras').ant_glob(var)
623			for x in lst:
624				if not x.name in ban:
625					load_tool(x.name.replace('.py', ''))
626		else:
627			from zipfile import PyZipFile
628			waflibs = PyZipFile(waf_dir)
629			lst = waflibs.namelist()
630			for x in lst:
631				if not re.match('waflib/extras/%s' % var.replace('*', '.*'), var):
632					continue
633				f = os.path.basename(x)
634				doban = False
635				for b in ban:
636					r = b.replace('*', '.*')
637					if re.match(r, f):
638						doban = True
639				if not doban:
640					f = f.replace('.py', '')
641					load_tool(f)
642
643cache_modules = {}
644"""
645Dictionary holding already loaded modules (wscript), indexed by their absolute path.
646The modules are added automatically by :py:func:`waflib.Context.load_module`
647"""
648
649def load_module(path, encoding=None):
650	"""
651	Loads a wscript file as a python module. This method caches results in :py:attr:`waflib.Context.cache_modules`
652
653	:param path: file path
654	:type path: string
655	:return: Loaded Python module
656	:rtype: module
657	"""
658	try:
659		return cache_modules[path]
660	except KeyError:
661		pass
662
663	module = imp.new_module(WSCRIPT_FILE)
664	try:
665		code = Utils.readf(path, m='r', encoding=encoding)
666	except EnvironmentError:
667		raise Errors.WafError('Could not read the file %r' % path)
668
669	module_dir = os.path.dirname(path)
670	sys.path.insert(0, module_dir)
671	try:
672		exec(compile(code, path, 'exec'), module.__dict__)
673	finally:
674		sys.path.remove(module_dir)
675
676	cache_modules[path] = module
677	return module
678
679def load_tool(tool, tooldir=None, ctx=None, with_sys_path=True):
680	"""
681	Imports a Waf tool as a python module, and stores it in the dict :py:const:`waflib.Context.Context.tools`
682
683	:type  tool: string
684	:param tool: Name of the tool
685	:type  tooldir: list
686	:param tooldir: List of directories to search for the tool module
687	:type  with_sys_path: boolean
688	:param with_sys_path: whether or not to search the regular sys.path, besides waf_dir and potentially given tooldirs
689	"""
690	if tool == 'java':
691		tool = 'javaw' # jython
692	else:
693		tool = tool.replace('++', 'xx')
694
695	if not with_sys_path:
696		back_path = sys.path
697		sys.path = []
698	try:
699		if tooldir:
700			assert isinstance(tooldir, list)
701			sys.path = tooldir + sys.path
702			try:
703				__import__(tool)
704			except ImportError as e:
705				e.waf_sys_path = list(sys.path)
706				raise
707			finally:
708				for d in tooldir:
709					sys.path.remove(d)
710			ret = sys.modules[tool]
711			Context.tools[tool] = ret
712			return ret
713		else:
714			if not with_sys_path:
715				sys.path.insert(0, waf_dir)
716			try:
717				for x in ('waflib.Tools.%s', 'waflib.extras.%s', 'waflib.%s', '%s'):
718					try:
719						__import__(x % tool)
720						break
721					except ImportError:
722						x = None
723				else: # raise an exception
724					__import__(tool)
725			except ImportError as e:
726				e.waf_sys_path = list(sys.path)
727				raise
728			finally:
729				if not with_sys_path:
730					sys.path.remove(waf_dir)
731			ret = sys.modules[x % tool]
732			Context.tools[tool] = ret
733			return ret
734	finally:
735		if not with_sys_path:
736			sys.path += back_path
737
738