1#!/usr/bin/env python
2# encoding: utf-8
3# Thomas Nagy, 2005-2018 (ita)
4
5"""
6logging, colors, terminal width and pretty-print
7"""
8
9import os, re, traceback, sys
10from waflib import Utils, ansiterm
11
12if not os.environ.get('NOSYNC', False):
13	# synchronized output is nearly mandatory to prevent garbled output
14	if sys.stdout.isatty() and id(sys.stdout) == id(sys.__stdout__):
15		sys.stdout = ansiterm.AnsiTerm(sys.stdout)
16	if sys.stderr.isatty() and id(sys.stderr) == id(sys.__stderr__):
17		sys.stderr = ansiterm.AnsiTerm(sys.stderr)
18
19# import the logging module after since it holds a reference on sys.stderr
20# in case someone uses the root logger
21import logging
22
23LOG_FORMAT = os.environ.get('WAF_LOG_FORMAT', '%(asctime)s %(c1)s%(zone)s%(c2)s %(message)s')
24HOUR_FORMAT = os.environ.get('WAF_HOUR_FORMAT', '%H:%M:%S')
25
26zones = []
27"""
28See :py:class:`waflib.Logs.log_filter`
29"""
30
31verbose = 0
32"""
33Global verbosity level, see :py:func:`waflib.Logs.debug` and :py:func:`waflib.Logs.error`
34"""
35
36colors_lst = {
37'USE' : True,
38'BOLD'  :'\x1b[01;1m',
39'RED'   :'\x1b[01;31m',
40'GREEN' :'\x1b[32m',
41'YELLOW':'\x1b[33m',
42'PINK'  :'\x1b[35m',
43'BLUE'  :'\x1b[01;34m',
44'CYAN'  :'\x1b[36m',
45'GREY'  :'\x1b[37m',
46'NORMAL':'\x1b[0m',
47'cursor_on'  :'\x1b[?25h',
48'cursor_off' :'\x1b[?25l',
49}
50
51indicator = '\r\x1b[K%s%s%s'
52
53try:
54	unicode
55except NameError:
56	unicode = None
57
58def enable_colors(use):
59	"""
60	If *1* is given, then the system will perform a few verifications
61	before enabling colors, such as checking whether the interpreter
62	is running in a terminal. A value of zero will disable colors,
63	and a value above *1* will force colors.
64
65	:param use: whether to enable colors or not
66	:type use: integer
67	"""
68	if use == 1:
69		if not (sys.stderr.isatty() or sys.stdout.isatty()):
70			use = 0
71		if Utils.is_win32 and os.name != 'java':
72			term = os.environ.get('TERM', '') # has ansiterm
73		else:
74			term = os.environ.get('TERM', 'dumb')
75
76		if term in ('dumb', 'emacs'):
77			use = 0
78
79	if use >= 1:
80		os.environ['TERM'] = 'vt100'
81
82	colors_lst['USE'] = use
83
84# If console packages are available, replace the dummy function with a real
85# implementation
86try:
87	get_term_cols = ansiterm.get_term_cols
88except AttributeError:
89	def get_term_cols():
90		return 80
91
92get_term_cols.__doc__ = """
93	Returns the console width in characters.
94
95	:return: the number of characters per line
96	:rtype: int
97	"""
98
99def get_color(cl):
100	"""
101	Returns the ansi sequence corresponding to the given color name.
102	An empty string is returned when coloring is globally disabled.
103
104	:param cl: color name in capital letters
105	:type cl: string
106	"""
107	if colors_lst['USE']:
108		return colors_lst.get(cl, '')
109	return ''
110
111class color_dict(object):
112	"""attribute-based color access, eg: colors.PINK"""
113	def __getattr__(self, a):
114		return get_color(a)
115	def __call__(self, a):
116		return get_color(a)
117
118colors = color_dict()
119
120re_log = re.compile(r'(\w+): (.*)', re.M)
121class log_filter(logging.Filter):
122	"""
123	Waf logs are of the form 'name: message', and can be filtered by 'waf --zones=name'.
124	For example, the following::
125
126		from waflib import Logs
127		Logs.debug('test: here is a message')
128
129	Will be displayed only when executing::
130
131		$ waf --zones=test
132	"""
133	def __init__(self, name=''):
134		logging.Filter.__init__(self, name)
135
136	def filter(self, rec):
137		"""
138		Filters log records by zone and by logging level
139
140		:param rec: log entry
141		"""
142		rec.zone = rec.module
143		if rec.levelno >= logging.INFO:
144			return True
145
146		m = re_log.match(rec.msg)
147		if m:
148			rec.zone = m.group(1)
149			rec.msg = m.group(2)
150
151		if zones:
152			return getattr(rec, 'zone', '') in zones or '*' in zones
153		elif not verbose > 2:
154			return False
155		return True
156
157class log_handler(logging.StreamHandler):
158	"""Dispatches messages to stderr/stdout depending on the severity level"""
159	def emit(self, record):
160		"""
161		Delegates the functionality to :py:meth:`waflib.Log.log_handler.emit_override`
162		"""
163		# default implementation
164		try:
165			try:
166				self.stream = record.stream
167			except AttributeError:
168				if record.levelno >= logging.WARNING:
169					record.stream = self.stream = sys.stderr
170				else:
171					record.stream = self.stream = sys.stdout
172			self.emit_override(record)
173			self.flush()
174		except (KeyboardInterrupt, SystemExit):
175			raise
176		except: # from the python library -_-
177			self.handleError(record)
178
179	def emit_override(self, record, **kw):
180		"""
181		Writes the log record to the desired stream (stderr/stdout)
182		"""
183		self.terminator = getattr(record, 'terminator', '\n')
184		stream = self.stream
185		if unicode:
186			# python2
187			msg = self.formatter.format(record)
188			fs = '%s' + self.terminator
189			try:
190				if (isinstance(msg, unicode) and getattr(stream, 'encoding', None)):
191					fs = fs.decode(stream.encoding)
192					try:
193						stream.write(fs % msg)
194					except UnicodeEncodeError:
195						stream.write((fs % msg).encode(stream.encoding))
196				else:
197					stream.write(fs % msg)
198			except UnicodeError:
199				stream.write((fs % msg).encode('utf-8'))
200		else:
201			logging.StreamHandler.emit(self, record)
202
203class formatter(logging.Formatter):
204	"""Simple log formatter which handles colors"""
205	def __init__(self):
206		logging.Formatter.__init__(self, LOG_FORMAT, HOUR_FORMAT)
207
208	def format(self, rec):
209		"""
210		Formats records and adds colors as needed. The records do not get
211		a leading hour format if the logging level is above *INFO*.
212		"""
213		try:
214			msg = rec.msg.decode('utf-8')
215		except Exception:
216			msg = rec.msg
217
218		use = colors_lst['USE']
219		if (use == 1 and rec.stream.isatty()) or use == 2:
220
221			c1 = getattr(rec, 'c1', None)
222			if c1 is None:
223				c1 = ''
224				if rec.levelno >= logging.ERROR:
225					c1 = colors.RED
226				elif rec.levelno >= logging.WARNING:
227					c1 = colors.YELLOW
228				elif rec.levelno >= logging.INFO:
229					c1 = colors.GREEN
230			c2 = getattr(rec, 'c2', colors.NORMAL)
231			msg = '%s%s%s' % (c1, msg, c2)
232		else:
233			# remove single \r that make long lines in text files
234			# and other terminal commands
235			msg = re.sub(r'\r(?!\n)|\x1B\[(K|.*?(m|h|l))', '', msg)
236
237		if rec.levelno >= logging.INFO:
238			# the goal of this is to format without the leading "Logs, hour" prefix
239			if rec.args:
240				try:
241					return msg % rec.args
242				except UnicodeDecodeError:
243					return msg.encode('utf-8') % rec.args
244			return msg
245
246		rec.msg = msg
247		rec.c1 = colors.PINK
248		rec.c2 = colors.NORMAL
249		return logging.Formatter.format(self, rec)
250
251log = None
252"""global logger for Logs.debug, Logs.error, etc"""
253
254def debug(*k, **kw):
255	"""
256	Wraps logging.debug and discards messages if the verbosity level :py:attr:`waflib.Logs.verbose` ≤ 0
257	"""
258	if verbose:
259		k = list(k)
260		k[0] = k[0].replace('\n', ' ')
261		log.debug(*k, **kw)
262
263def error(*k, **kw):
264	"""
265	Wrap logging.errors, adds the stack trace when the verbosity level :py:attr:`waflib.Logs.verbose` ≥ 2
266	"""
267	log.error(*k, **kw)
268	if verbose > 2:
269		st = traceback.extract_stack()
270		if st:
271			st = st[:-1]
272			buf = []
273			for filename, lineno, name, line in st:
274				buf.append('  File %r, line %d, in %s' % (filename, lineno, name))
275				if line:
276					buf.append('	%s' % line.strip())
277			if buf:
278				log.error('\n'.join(buf))
279
280def warn(*k, **kw):
281	"""
282	Wraps logging.warning
283	"""
284	log.warning(*k, **kw)
285
286def info(*k, **kw):
287	"""
288	Wraps logging.info
289	"""
290	log.info(*k, **kw)
291
292def init_log():
293	"""
294	Initializes the logger :py:attr:`waflib.Logs.log`
295	"""
296	global log
297	log = logging.getLogger('waflib')
298	log.handlers = []
299	log.filters = []
300	hdlr = log_handler()
301	hdlr.setFormatter(formatter())
302	log.addHandler(hdlr)
303	log.addFilter(log_filter())
304	log.setLevel(logging.DEBUG)
305
306def make_logger(path, name):
307	"""
308	Creates a simple logger, which is often used to redirect the context command output::
309
310		from waflib import Logs
311		bld.logger = Logs.make_logger('test.log', 'build')
312		bld.check(header_name='sadlib.h', features='cxx cprogram', mandatory=False)
313
314		# have the file closed immediately
315		Logs.free_logger(bld.logger)
316
317		# stop logging
318		bld.logger = None
319
320	The method finalize() of the command will try to free the logger, if any
321
322	:param path: file name to write the log output to
323	:type path: string
324	:param name: logger name (loggers are reused)
325	:type name: string
326	"""
327	logger = logging.getLogger(name)
328	if sys.hexversion > 0x3000000:
329		encoding = sys.stdout.encoding
330	else:
331		encoding = None
332	hdlr = logging.FileHandler(path, 'w', encoding=encoding)
333	formatter = logging.Formatter('%(message)s')
334	hdlr.setFormatter(formatter)
335	logger.addHandler(hdlr)
336	logger.setLevel(logging.DEBUG)
337	return logger
338
339def make_mem_logger(name, to_log, size=8192):
340	"""
341	Creates a memory logger to avoid writing concurrently to the main logger
342	"""
343	from logging.handlers import MemoryHandler
344	logger = logging.getLogger(name)
345	hdlr = MemoryHandler(size, target=to_log)
346	formatter = logging.Formatter('%(message)s')
347	hdlr.setFormatter(formatter)
348	logger.addHandler(hdlr)
349	logger.memhandler = hdlr
350	logger.setLevel(logging.DEBUG)
351	return logger
352
353def free_logger(logger):
354	"""
355	Frees the resources held by the loggers created through make_logger or make_mem_logger.
356	This is used for file cleanup and for handler removal (logger objects are re-used).
357	"""
358	try:
359		for x in logger.handlers:
360			x.close()
361			logger.removeHandler(x)
362	except Exception:
363		pass
364
365def pprint(col, msg, label='', sep='\n'):
366	"""
367	Prints messages in color immediately on stderr::
368
369		from waflib import Logs
370		Logs.pprint('RED', 'Something bad just happened')
371
372	:param col: color name to use in :py:const:`Logs.colors_lst`
373	:type col: string
374	:param msg: message to display
375	:type msg: string or a value that can be printed by %s
376	:param label: a message to add after the colored output
377	:type label: string
378	:param sep: a string to append at the end (line separator)
379	:type sep: string
380	"""
381	info('%s%s%s %s', colors(col), msg, colors.NORMAL, label, extra={'terminator':sep})
382
383