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