waf

FORK: waf with some random patches
git clone https://git.neptards.moe/neptards/waf.git
Log | Files | Refs | README

Logs.py (9755B)


      1 #!/usr/bin/env python
      2 # encoding: utf-8
      3 # Thomas Nagy, 2005-2018 (ita)
      4 
      5 """
      6 logging, colors, terminal width and pretty-print
      7 """
      8 
      9 import os, re, traceback, sys
     10 from waflib import Utils, ansiterm
     11 
     12 if 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
     21 import logging
     22 
     23 LOG_FORMAT = os.environ.get('WAF_LOG_FORMAT', '%(asctime)s %(c1)s%(zone)s%(c2)s %(message)s')
     24 HOUR_FORMAT = os.environ.get('WAF_HOUR_FORMAT', '%H:%M:%S')
     25 
     26 zones = []
     27 """
     28 See :py:class:`waflib.Logs.log_filter`
     29 """
     30 
     31 verbose = 0
     32 """
     33 Global verbosity level, see :py:func:`waflib.Logs.debug` and :py:func:`waflib.Logs.error`
     34 """
     35 
     36 colors_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 
     51 indicator = '\r\x1b[K%s%s%s'
     52 
     53 try:
     54 	unicode
     55 except NameError:
     56 	unicode = None
     57 
     58 def 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
     86 try:
     87 	get_term_cols = ansiterm.get_term_cols
     88 except AttributeError:
     89 	def get_term_cols():
     90 		return 80
     91 
     92 get_term_cols.__doc__ = """
     93 	Returns the console width in characters.
     94 
     95 	:return: the number of characters per line
     96 	:rtype: int
     97 	"""
     98 
     99 def 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 
    111 class 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 
    118 colors = color_dict()
    119 
    120 re_log = re.compile(r'(\w+): (.*)', re.M)
    121 class 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 
    157 class 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 
    203 class 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 
    251 log = None
    252 """global logger for Logs.debug, Logs.error, etc"""
    253 
    254 def 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 
    263 def 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 
    280 def warn(*k, **kw):
    281 	"""
    282 	Wraps logging.warning
    283 	"""
    284 	log.warning(*k, **kw)
    285 
    286 def info(*k, **kw):
    287 	"""
    288 	Wraps logging.info
    289 	"""
    290 	log.info(*k, **kw)
    291 
    292 def 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 
    306 def 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 
    339 def 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 
    353 def 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 
    365 def 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