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