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