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 return msg % rec.args 241 return msg 242 243 rec.msg = msg 244 rec.c1 = colors.PINK 245 rec.c2 = colors.NORMAL 246 return logging.Formatter.format(self, rec) 247 248log = None 249"""global logger for Logs.debug, Logs.error, etc""" 250 251def debug(*k, **kw): 252 """ 253 Wraps logging.debug and discards messages if the verbosity level :py:attr:`waflib.Logs.verbose` ≤ 0 254 """ 255 if verbose: 256 k = list(k) 257 k[0] = k[0].replace('\n', ' ') 258 log.debug(*k, **kw) 259 260def error(*k, **kw): 261 """ 262 Wrap logging.errors, adds the stack trace when the verbosity level :py:attr:`waflib.Logs.verbose` ≥ 2 263 """ 264 log.error(*k, **kw) 265 if verbose > 2: 266 st = traceback.extract_stack() 267 if st: 268 st = st[:-1] 269 buf = [] 270 for filename, lineno, name, line in st: 271 buf.append(' File %r, line %d, in %s' % (filename, lineno, name)) 272 if line: 273 buf.append(' %s' % line.strip()) 274 if buf: 275 log.error('\n'.join(buf)) 276 277def warn(*k, **kw): 278 """ 279 Wraps logging.warn 280 """ 281 log.warn(*k, **kw) 282 283def info(*k, **kw): 284 """ 285 Wraps logging.info 286 """ 287 log.info(*k, **kw) 288 289def init_log(): 290 """ 291 Initializes the logger :py:attr:`waflib.Logs.log` 292 """ 293 global log 294 log = logging.getLogger('waflib') 295 log.handlers = [] 296 log.filters = [] 297 hdlr = log_handler() 298 hdlr.setFormatter(formatter()) 299 log.addHandler(hdlr) 300 log.addFilter(log_filter()) 301 log.setLevel(logging.DEBUG) 302 303def make_logger(path, name): 304 """ 305 Creates a simple logger, which is often used to redirect the context command output:: 306 307 from waflib import Logs 308 bld.logger = Logs.make_logger('test.log', 'build') 309 bld.check(header_name='sadlib.h', features='cxx cprogram', mandatory=False) 310 311 # have the file closed immediately 312 Logs.free_logger(bld.logger) 313 314 # stop logging 315 bld.logger = None 316 317 The method finalize() of the command will try to free the logger, if any 318 319 :param path: file name to write the log output to 320 :type path: string 321 :param name: logger name (loggers are reused) 322 :type name: string 323 """ 324 logger = logging.getLogger(name) 325 if sys.hexversion > 0x3000000: 326 encoding = sys.stdout.encoding 327 else: 328 encoding = None 329 hdlr = logging.FileHandler(path, 'w', encoding=encoding) 330 formatter = logging.Formatter('%(message)s') 331 hdlr.setFormatter(formatter) 332 logger.addHandler(hdlr) 333 logger.setLevel(logging.DEBUG) 334 return logger 335 336def make_mem_logger(name, to_log, size=8192): 337 """ 338 Creates a memory logger to avoid writing concurrently to the main logger 339 """ 340 from logging.handlers import MemoryHandler 341 logger = logging.getLogger(name) 342 hdlr = MemoryHandler(size, target=to_log) 343 formatter = logging.Formatter('%(message)s') 344 hdlr.setFormatter(formatter) 345 logger.addHandler(hdlr) 346 logger.memhandler = hdlr 347 logger.setLevel(logging.DEBUG) 348 return logger 349 350def free_logger(logger): 351 """ 352 Frees the resources held by the loggers created through make_logger or make_mem_logger. 353 This is used for file cleanup and for handler removal (logger objects are re-used). 354 """ 355 try: 356 for x in logger.handlers: 357 x.close() 358 logger.removeHandler(x) 359 except Exception: 360 pass 361 362def pprint(col, msg, label='', sep='\n'): 363 """ 364 Prints messages in color immediately on stderr:: 365 366 from waflib import Logs 367 Logs.pprint('RED', 'Something bad just happened') 368 369 :param col: color name to use in :py:const:`Logs.colors_lst` 370 :type col: string 371 :param msg: message to display 372 :type msg: string or a value that can be printed by %s 373 :param label: a message to add after the colored output 374 :type label: string 375 :param sep: a string to append at the end (line separator) 376 :type sep: string 377 """ 378 info('%s%s%s %s', colors(col), msg, colors.NORMAL, label, extra={'terminator':sep}) 379 380