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