#!/usr/bin/env python # encoding: utf-8 # Thomas Nagy, 2010-2018 (ita) """ Classes and functions enabling the command system """ import os, re, imp, sys from waflib import Utils, Errors, Logs import waflib.Node # the following 3 constants are updated on each new release (do not touch) HEXVERSION=0x2000b00 """Constant updated on new releases""" WAFVERSION="2.0.11" """Constant updated on new releases""" WAFREVISION="a97f6fb0941091b4966b625f15ec32fa783a8bec" """Git revision when the waf version is updated""" ABI = 20 """Version of the build data cache file format (used in :py:const:`waflib.Context.DBFILE`)""" DBFILE = '.wafpickle-%s-%d-%d' % (sys.platform, sys.hexversion, ABI) """Name of the pickle file for storing the build data""" APPNAME = 'APPNAME' """Default application name (used by ``waf dist``)""" VERSION = 'VERSION' """Default application version (used by ``waf dist``)""" TOP = 'top' """The variable name for the top-level directory in wscript files""" OUT = 'out' """The variable name for the output directory in wscript files""" WSCRIPT_FILE = 'wscript' """Name of the waf script files""" launch_dir = '' """Directory from which waf has been called""" run_dir = '' """Location of the wscript file to use as the entry point""" top_dir = '' """Location of the project directory (top), if the project was configured""" out_dir = '' """Location of the build directory (out), if the project was configured""" waf_dir = '' """Directory containing the waf modules""" default_encoding = Utils.console_encoding() """Encoding to use when reading outputs from other processes""" g_module = None """ Module representing the top-level wscript file (see :py:const:`waflib.Context.run_dir`) """ STDOUT = 1 STDERR = -1 BOTH = 0 classes = [] """ List of :py:class:`waflib.Context.Context` subclasses that can be used as waf commands. The classes are added automatically by a metaclass. """ def create_context(cmd_name, *k, **kw): """ Returns a new :py:class:`waflib.Context.Context` instance corresponding to the given command. Used in particular by :py:func:`waflib.Scripting.run_command` :param cmd_name: command name :type cmd_name: string :param k: arguments to give to the context class initializer :type k: list :param k: keyword arguments to give to the context class initializer :type k: dict :return: Context object :rtype: :py:class:`waflib.Context.Context` """ for x in classes: if x.cmd == cmd_name: return x(*k, **kw) ctx = Context(*k, **kw) ctx.fun = cmd_name return ctx class store_context(type): """ Metaclass that registers command classes into the list :py:const:`waflib.Context.classes` Context classes must provide an attribute 'cmd' representing the command name, and a function attribute 'fun' representing the function name that the command uses. """ def __init__(cls, name, bases, dct): super(store_context, cls).__init__(name, bases, dct) name = cls.__name__ if name in ('ctx', 'Context'): return try: cls.cmd except AttributeError: raise Errors.WafError('Missing command for the context class %r (cmd)' % name) if not getattr(cls, 'fun', None): cls.fun = cls.cmd classes.insert(0, cls) ctx = store_context('ctx', (object,), {}) """Base class for all :py:class:`waflib.Context.Context` classes""" class Context(ctx): """ Default context for waf commands, and base class for new command contexts. Context objects are passed to top-level functions:: def foo(ctx): print(ctx.__class__.__name__) # waflib.Context.Context Subclasses must define the class attributes 'cmd' and 'fun': :param cmd: command to execute as in ``waf cmd`` :type cmd: string :param fun: function name to execute when the command is called :type fun: string .. 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 """ errors = Errors """ Shortcut to :py:mod:`waflib.Errors` provided for convenience """ tools = {} """ A module cache for wscript files; see :py:meth:`Context.Context.load` """ def __init__(self, **kw): try: rd = kw['run_dir'] except KeyError: rd = run_dir # binds the context to the nodes in use to avoid a context singleton self.node_class = type('Nod3', (waflib.Node.Node,), {}) self.node_class.__module__ = 'waflib.Node' self.node_class.ctx = self self.root = self.node_class('', None) self.cur_script = None self.path = self.root.find_dir(rd) self.stack_path = [] self.exec_dict = {'ctx':self, 'conf':self, 'bld':self, 'opt':self} self.logger = None def finalize(self): """ Called to free resources such as logger files """ try: logger = self.logger except AttributeError: pass else: Logs.free_logger(logger) delattr(self, 'logger') def load(self, tool_list, *k, **kw): """ Loads a Waf tool as a module, and try calling the function named :py:const:`waflib.Context.Context.fun` from it. A ``tooldir`` argument may be provided as a list of module paths. :param tool_list: list of Waf tool names to load :type tool_list: list of string or space-separated string """ tools = Utils.to_list(tool_list) path = Utils.to_list(kw.get('tooldir', '')) with_sys_path = kw.get('with_sys_path', True) for t in tools: module = load_tool(t, path, with_sys_path=with_sys_path) fun = getattr(module, kw.get('name', self.fun), None) if fun: fun(self) def execute(self): """ Here, it calls the function name in the top-level wscript file. Most subclasses redefine this method to provide additional functionality. """ self.recurse([os.path.dirname(g_module.root_path)]) def pre_recurse(self, node): """ Method executed immediately before a folder is read by :py:meth:`waflib.Context.Context.recurse`. The current script is bound as a Node object on ``self.cur_script``, and the current path is bound to ``self.path`` :param node: script :type node: :py:class:`waflib.Node.Node` """ self.stack_path.append(self.cur_script) self.cur_script = node self.path = node.parent def post_recurse(self, node): """ Restores ``self.cur_script`` and ``self.path`` right after :py:meth:`waflib.Context.Context.recurse` terminates. :param node: script :type node: :py:class:`waflib.Node.Node` """ self.cur_script = self.stack_path.pop() if self.cur_script: self.path = self.cur_script.parent def recurse(self, dirs, name=None, mandatory=True, once=True, encoding=None): """ Runs user-provided functions from the supplied list of directories. The directories can be either absolute, or relative to the directory of the wscript file The methods :py:meth:`waflib.Context.Context.pre_recurse` and :py:meth:`waflib.Context.Context.post_recurse` are called immediately before and after a script has been executed. :param dirs: List of directories to visit :type dirs: list of string or space-separated string :param name: Name of function to invoke from the wscript :type name: string :param mandatory: whether sub wscript files are required to exist :type mandatory: bool :param once: read the script file once for a particular context :type once: bool """ try: cache = self.recurse_cache except AttributeError: cache = self.recurse_cache = {} for d in Utils.to_list(dirs): if not os.path.isabs(d): # absolute paths only d = os.path.join(self.path.abspath(), d) WSCRIPT = os.path.join(d, WSCRIPT_FILE) WSCRIPT_FUN = WSCRIPT + '_' + (name or self.fun) node = self.root.find_node(WSCRIPT_FUN) if node and (not once or node not in cache): cache[node] = True self.pre_recurse(node) try: function_code = node.read('rU', encoding) exec(compile(function_code, node.abspath(), 'exec'), self.exec_dict) finally: self.post_recurse(node) elif not node: node = self.root.find_node(WSCRIPT) tup = (node, name or self.fun) if node and (not once or tup not in cache): cache[tup] = True self.pre_recurse(node) try: wscript_module = load_module(node.abspath(), encoding=encoding) user_function = getattr(wscript_module, (name or self.fun), None) if not user_function: if not mandatory: continue raise Errors.WafError('No function %r defined in %s' % (name or self.fun, node.abspath())) user_function(self) finally: self.post_recurse(node) elif not node: if not mandatory: continue try: os.listdir(d) except OSError: raise Errors.WafError('Cannot read the folder %r' % d) raise Errors.WafError('No wscript file in directory %s' % d) def log_command(self, cmd, kw): if Logs.verbose: fmt = os.environ.get('WAF_CMD_FORMAT') if fmt == 'string': if not isinstance(cmd, str): cmd = Utils.shell_escape(cmd) Logs.debug('runner: %r', cmd) Logs.debug('runner_env: kw=%s', kw) def exec_command(self, cmd, **kw): """ Runs an external process and returns the exit status:: def run(tsk): ret = tsk.generator.bld.exec_command('touch foo.txt') return ret If the context has the attribute 'log', then captures and logs the process stderr/stdout. Unlike :py:meth:`waflib.Context.Context.cmd_and_log`, this method does not return the stdout/stderr values captured. :param cmd: command argument for subprocess.Popen :type cmd: string or list :param kw: keyword arguments for subprocess.Popen. The parameters input/timeout will be passed to wait/communicate. :type kw: dict :returns: process exit status :rtype: integer :raises: :py:class:`waflib.Errors.WafError` if an invalid executable is specified for a non-shell process :raises: :py:class:`waflib.Errors.WafError` in case of execution failure """ subprocess = Utils.subprocess kw['shell'] = isinstance(cmd, str) self.log_command(cmd, kw) if self.logger: self.logger.info(cmd) if 'stdout' not in kw: kw['stdout'] = subprocess.PIPE if 'stderr' not in kw: kw['stderr'] = subprocess.PIPE if Logs.verbose and not kw['shell'] and not Utils.check_exe(cmd[0]): raise Errors.WafError('Program %s not found!' % cmd[0]) cargs = {} if 'timeout' in kw: if sys.hexversion >= 0x3030000: cargs['timeout'] = kw['timeout'] if not 'start_new_session' in kw: kw['start_new_session'] = True del kw['timeout'] if 'input' in kw: if kw['input']: cargs['input'] = kw['input'] kw['stdin'] = subprocess.PIPE del kw['input'] if 'cwd' in kw: if not isinstance(kw['cwd'], str): kw['cwd'] = kw['cwd'].abspath() encoding = kw.pop('decode_as', default_encoding) try: ret, out, err = Utils.run_process(cmd, kw, cargs) except Exception as e: raise Errors.WafError('Execution failure: %s' % str(e), ex=e) if out: if not isinstance(out, str): out = out.decode(encoding, errors='replace') if self.logger: self.logger.debug('out: %s', out) else: Logs.info(out, extra={'stream':sys.stdout, 'c1': ''}) if err: if not isinstance(err, str): err = err.decode(encoding, errors='replace') if self.logger: self.logger.error('err: %s' % err) else: Logs.info(err, extra={'stream':sys.stderr, 'c1': ''}) return ret def cmd_and_log(self, cmd, **kw): """ Executes a process and returns stdout/stderr if the execution is successful. An exception is thrown when the exit status is non-0. In that case, both stderr and stdout will be bound to the WafError object (configuration tests):: def configure(conf): out = conf.cmd_and_log(['echo', 'hello'], output=waflib.Context.STDOUT, quiet=waflib.Context.BOTH) (out, err) = conf.cmd_and_log(['echo', 'hello'], output=waflib.Context.BOTH) (out, err) = conf.cmd_and_log(cmd, input='\\n'.encode(), output=waflib.Context.STDOUT) try: conf.cmd_and_log(['which', 'someapp'], output=waflib.Context.BOTH) except Errors.WafError as e: print(e.stdout, e.stderr) :param cmd: args for subprocess.Popen :type cmd: list or string :param kw: keyword arguments for subprocess.Popen. The parameters input/timeout will be passed to wait/communicate. :type kw: dict :returns: a tuple containing the contents of stdout and stderr :rtype: string :raises: :py:class:`waflib.Errors.WafError` if an invalid executable is specified for a non-shell process :raises: :py:class:`waflib.Errors.WafError` in case of execution failure; stdout/stderr/returncode are bound to the exception object """ subprocess = Utils.subprocess kw['shell'] = isinstance(cmd, str) self.log_command(cmd, kw) quiet = kw.pop('quiet', None) to_ret = kw.pop('output', STDOUT) if Logs.verbose and not kw['shell'] and not Utils.check_exe(cmd[0]): raise Errors.WafError('Program %r not found!' % cmd[0]) kw['stdout'] = kw['stderr'] = subprocess.PIPE if quiet is None: self.to_log(cmd) cargs = {} if 'timeout' in kw: if sys.hexversion >= 0x3030000: cargs['timeout'] = kw['timeout'] if not 'start_new_session' in kw: kw['start_new_session'] = True del kw['timeout'] if 'input' in kw: if kw['input']: cargs['input'] = kw['input'] kw['stdin'] = subprocess.PIPE del kw['input'] if 'cwd' in kw: if not isinstance(kw['cwd'], str): kw['cwd'] = kw['cwd'].abspath() encoding = kw.pop('decode_as', default_encoding) try: ret, out, err = Utils.run_process(cmd, kw, cargs) except Exception as e: raise Errors.WafError('Execution failure: %s' % str(e), ex=e) if not isinstance(out, str): out = out.decode(encoding, errors='replace') if not isinstance(err, str): err = err.decode(encoding, errors='replace') if out and quiet != STDOUT and quiet != BOTH: self.to_log('out: %s' % out) if err and quiet != STDERR and quiet != BOTH: self.to_log('err: %s' % err) if ret: e = Errors.WafError('Command %r returned %r' % (cmd, ret)) e.returncode = ret e.stderr = err e.stdout = out raise e if to_ret == BOTH: return (out, err) elif to_ret == STDERR: return err return out def fatal(self, msg, ex=None): """ Prints an error message in red and stops command execution; this is usually used in the configuration section:: def configure(conf): conf.fatal('a requirement is missing') :param msg: message to display :type msg: string :param ex: optional exception object :type ex: exception :raises: :py:class:`waflib.Errors.ConfigurationError` """ if self.logger: self.logger.info('from %s: %s' % (self.path.abspath(), msg)) try: logfile = self.logger.handlers[0].baseFilename except AttributeError: pass else: if os.environ.get('WAF_PRINT_FAILURE_LOG'): # see #1930 msg = 'Log from (%s):\n%s\n' % (logfile, Utils.readf(logfile)) else: msg = '%s\n(complete log in %s)' % (msg, logfile) raise self.errors.ConfigurationError(msg, ex=ex) def to_log(self, msg): """ Logs information to the logger (if present), or to stderr. Empty messages are not printed:: def build(bld): bld.to_log('starting the build') Provide a logger on the context class or override this method if necessary. :param msg: message :type msg: string """ if not msg: return if self.logger: self.logger.info(msg) else: sys.stderr.write(str(msg)) sys.stderr.flush() def msg(self, *k, **kw): """ Prints a configuration message of the form ``msg: result``. The second part of the message will be in colors. The output can be disabled easly by setting ``in_msg`` to a positive value:: def configure(conf): self.in_msg = 1 conf.msg('Checking for library foo', 'ok') # no output :param msg: message to display to the user :type msg: string :param result: result to display :type result: string or boolean :param color: color to use, see :py:const:`waflib.Logs.colors_lst` :type color: string """ try: msg = kw['msg'] except KeyError: msg = k[0] self.start_msg(msg, **kw) try: result = kw['result'] except KeyError: result = k[1] color = kw.get('color') if not isinstance(color, str): color = result and 'GREEN' or 'YELLOW' self.end_msg(result, color, **kw) def start_msg(self, *k, **kw): """ Prints the beginning of a 'Checking for xxx' message. See :py:meth:`waflib.Context.Context.msg` """ if kw.get('quiet'): return msg = kw.get('msg') or k[0] try: if self.in_msg: self.in_msg += 1 return except AttributeError: self.in_msg = 0 self.in_msg += 1 try: self.line_just = max(self.line_just, len(msg)) except AttributeError: self.line_just = max(40, len(msg)) for x in (self.line_just * '-', msg): self.to_log(x) Logs.pprint('NORMAL', "%s :" % msg.ljust(self.line_just), sep='') def end_msg(self, *k, **kw): """Prints the end of a 'Checking for' message. See :py:meth:`waflib.Context.Context.msg`""" if kw.get('quiet'): return self.in_msg -= 1 if self.in_msg: return result = kw.get('result') or k[0] defcolor = 'GREEN' if result is True: msg = 'ok' elif not result: msg = 'not found' defcolor = 'YELLOW' else: msg = str(result) self.to_log(msg) try: color = kw['color'] except KeyError: if len(k) > 1 and k[1] in Logs.colors_lst: # compatibility waf 1.7 color = k[1] else: color = defcolor Logs.pprint(color, msg) def load_special_tools(self, var, ban=[]): """ Loads third-party extensions modules for certain programming languages by trying to list certain files in the extras/ directory. This method is typically called once for a programming language group, see for example :py:mod:`waflib.Tools.compiler_c` :param var: glob expression, for example 'cxx\_\*.py' :type var: string :param ban: list of exact file names to exclude :type ban: list of string """ if os.path.isdir(waf_dir): lst = self.root.find_node(waf_dir).find_node('waflib/extras').ant_glob(var) for x in lst: if not x.name in ban: load_tool(x.name.replace('.py', '')) else: from zipfile import PyZipFile waflibs = PyZipFile(waf_dir) lst = waflibs.namelist() for x in lst: if not re.match('waflib/extras/%s' % var.replace('*', '.*'), var): continue f = os.path.basename(x) doban = False for b in ban: r = b.replace('*', '.*') if re.match(r, f): doban = True if not doban: f = f.replace('.py', '') load_tool(f) cache_modules = {} """ Dictionary holding already loaded modules (wscript), indexed by their absolute path. The modules are added automatically by :py:func:`waflib.Context.load_module` """ def load_module(path, encoding=None): """ Loads a wscript file as a python module. This method caches results in :py:attr:`waflib.Context.cache_modules` :param path: file path :type path: string :return: Loaded Python module :rtype: module """ try: return cache_modules[path] except KeyError: pass module = imp.new_module(WSCRIPT_FILE) try: code = Utils.readf(path, m='rU', encoding=encoding) except EnvironmentError: raise Errors.WafError('Could not read the file %r' % path) module_dir = os.path.dirname(path) sys.path.insert(0, module_dir) try: exec(compile(code, path, 'exec'), module.__dict__) finally: sys.path.remove(module_dir) cache_modules[path] = module return module def load_tool(tool, tooldir=None, ctx=None, with_sys_path=True): """ Importx a Waf tool as a python module, and stores it in the dict :py:const:`waflib.Context.Context.tools` :type tool: string :param tool: Name of the tool :type tooldir: list :param tooldir: List of directories to search for the tool module :type with_sys_path: boolean :param with_sys_path: whether or not to search the regular sys.path, besides waf_dir and potentially given tooldirs """ if tool == 'java': tool = 'javaw' # jython else: tool = tool.replace('++', 'xx') if not with_sys_path: back_path = sys.path sys.path = [] try: if tooldir: assert isinstance(tooldir, list) sys.path = tooldir + sys.path try: __import__(tool) except ImportError as e: e.waf_sys_path = list(sys.path) raise finally: for d in tooldir: sys.path.remove(d) ret = sys.modules[tool] Context.tools[tool] = ret return ret else: if not with_sys_path: sys.path.insert(0, waf_dir) try: for x in ('waflib.Tools.%s', 'waflib.extras.%s', 'waflib.%s', '%s'): try: __import__(x % tool) break except ImportError: x = None else: # raise an exception __import__(tool) except ImportError as e: e.waf_sys_path = list(sys.path) raise finally: if not with_sys_path: sys.path.remove(waf_dir) ret = sys.modules[x % tool] Context.tools[tool] = ret return ret finally: if not with_sys_path: sys.path += back_path