diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /python/mozbuild/mozbuild/configure | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'python/mozbuild/mozbuild/configure')
-rw-r--r-- | python/mozbuild/mozbuild/configure/__init__.py | 935 | ||||
-rw-r--r-- | python/mozbuild/mozbuild/configure/check_debug_ranges.py | 62 | ||||
-rw-r--r-- | python/mozbuild/mozbuild/configure/constants.py | 103 | ||||
-rw-r--r-- | python/mozbuild/mozbuild/configure/help.py | 45 | ||||
-rw-r--r-- | python/mozbuild/mozbuild/configure/libstdcxx.py | 81 | ||||
-rw-r--r-- | python/mozbuild/mozbuild/configure/lint.py | 78 | ||||
-rw-r--r-- | python/mozbuild/mozbuild/configure/lint_util.py | 52 | ||||
-rw-r--r-- | python/mozbuild/mozbuild/configure/options.py | 485 | ||||
-rw-r--r-- | python/mozbuild/mozbuild/configure/util.py | 226 |
9 files changed, 2067 insertions, 0 deletions
diff --git a/python/mozbuild/mozbuild/configure/__init__.py b/python/mozbuild/mozbuild/configure/__init__.py new file mode 100644 index 000000000..0fe640cae --- /dev/null +++ b/python/mozbuild/mozbuild/configure/__init__.py @@ -0,0 +1,935 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from __future__ import absolute_import, print_function, unicode_literals + +import inspect +import logging +import os +import re +import sys +import types +from collections import OrderedDict +from contextlib import contextmanager +from functools import wraps +from mozbuild.configure.options import ( + CommandLineHelper, + ConflictingOptionError, + InvalidOptionError, + NegativeOptionValue, + Option, + OptionValue, + PositiveOptionValue, +) +from mozbuild.configure.help import HelpFormatter +from mozbuild.configure.util import ( + ConfigureOutputHandler, + getpreferredencoding, + LineIO, +) +from mozbuild.util import ( + exec_, + memoize, + memoized_property, + ReadOnlyDict, + ReadOnlyNamespace, +) + +import mozpack.path as mozpath + + +class ConfigureError(Exception): + pass + + +class SandboxDependsFunction(object): + '''Sandbox-visible representation of @depends functions.''' + def __call__(self, *arg, **kwargs): + raise ConfigureError('The `%s` function may not be called' + % self.__name__) + + +class DependsFunction(object): + __slots__ = ( + 'func', 'dependencies', 'when', 'sandboxed', 'sandbox', '_result') + + def __init__(self, sandbox, func, dependencies, when=None): + assert isinstance(sandbox, ConfigureSandbox) + self.func = func + self.dependencies = dependencies + self.sandboxed = wraps(func)(SandboxDependsFunction()) + self.sandbox = sandbox + self.when = when + sandbox._depends[self.sandboxed] = self + + # Only @depends functions with a dependency on '--help' are executed + # immediately. Everything else is queued for later execution. + if sandbox._help_option in dependencies: + sandbox._value_for(self) + elif not sandbox._help: + sandbox._execution_queue.append((sandbox._value_for, (self,))) + + @property + def name(self): + return self.func.__name__ + + @property + def sandboxed_dependencies(self): + return [ + d.sandboxed if isinstance(d, DependsFunction) else d + for d in self.dependencies + ] + + @memoized_property + def result(self): + if self.when and not self.sandbox._value_for(self.when): + return None + + resolved_args = [self.sandbox._value_for(d) for d in self.dependencies] + return self.func(*resolved_args) + + def __repr__(self): + return '<%s.%s %s(%s)>' % ( + self.__class__.__module__, + self.__class__.__name__, + self.name, + ', '.join(repr(d) for d in self.dependencies), + ) + + +class CombinedDependsFunction(DependsFunction): + def __init__(self, sandbox, func, dependencies): + @memoize + @wraps(func) + def wrapper(*args): + return func(args) + + flatten_deps = [] + for d in dependencies: + if isinstance(d, CombinedDependsFunction) and d.func == wrapper: + for d2 in d.dependencies: + if d2 not in flatten_deps: + flatten_deps.append(d2) + elif d not in flatten_deps: + flatten_deps.append(d) + + # Automatically add a --help dependency if one of the dependencies + # depends on it. + for d in flatten_deps: + if (isinstance(d, DependsFunction) and + sandbox._help_option in d.dependencies): + flatten_deps.insert(0, sandbox._help_option) + break + + super(CombinedDependsFunction, self).__init__( + sandbox, wrapper, flatten_deps) + + @memoized_property + def result(self): + # Ignore --help for the combined result + deps = self.dependencies + if deps[0] == self.sandbox._help_option: + deps = deps[1:] + resolved_args = [self.sandbox._value_for(d) for d in deps] + return self.func(*resolved_args) + + def __eq__(self, other): + return (isinstance(other, self.__class__) and + self.func == other.func and + set(self.dependencies) == set(other.dependencies)) + + def __ne__(self, other): + return not self == other + +class SandboxedGlobal(dict): + '''Identifiable dict type for use as function global''' + + +def forbidden_import(*args, **kwargs): + raise ImportError('Importing modules is forbidden') + + +class ConfigureSandbox(dict): + """Represents a sandbox for executing Python code for build configuration. + This is a different kind of sandboxing than the one used for moz.build + processing. + + The sandbox has 9 primitives: + - option + - depends + - template + - imports + - include + - set_config + - set_define + - imply_option + - only_when + + `option`, `include`, `set_config`, `set_define` and `imply_option` are + functions. `depends`, `template`, and `imports` are decorators. `only_when` + is a context_manager. + + These primitives are declared as name_impl methods to this class and + the mapping name -> name_impl is done automatically in __getitem__. + + Additional primitives should be frowned upon to keep the sandbox itself as + simple as possible. Instead, helpers should be created within the sandbox + with the existing primitives. + + The sandbox is given, at creation, a dict where the yielded configuration + will be stored. + + config = {} + sandbox = ConfigureSandbox(config) + sandbox.run(path) + do_stuff(config) + """ + + # The default set of builtins. We expose unicode as str to make sandboxed + # files more python3-ready. + BUILTINS = ReadOnlyDict({ + b: __builtins__[b] + for b in ('None', 'False', 'True', 'int', 'bool', 'any', 'all', 'len', + 'list', 'tuple', 'set', 'dict', 'isinstance', 'getattr', + 'hasattr', 'enumerate', 'range', 'zip') + }, __import__=forbidden_import, str=unicode) + + # Expose a limited set of functions from os.path + OS = ReadOnlyNamespace(path=ReadOnlyNamespace(**{ + k: getattr(mozpath, k, getattr(os.path, k)) + for k in ('abspath', 'basename', 'dirname', 'isabs', 'join', + 'normcase', 'normpath', 'realpath', 'relpath') + })) + + def __init__(self, config, environ=os.environ, argv=sys.argv, + stdout=sys.stdout, stderr=sys.stderr, logger=None): + dict.__setitem__(self, '__builtins__', self.BUILTINS) + + self._paths = [] + self._all_paths = set() + self._templates = set() + # Associate SandboxDependsFunctions to DependsFunctions. + self._depends = {} + self._seen = set() + # Store the @imports added to a given function. + self._imports = {} + + self._options = OrderedDict() + # Store raw option (as per command line or environment) for each Option + self._raw_options = OrderedDict() + + # Store options added with `imply_option`, and the reason they were + # added (which can either have been given to `imply_option`, or + # inferred. Their order matters, so use a list. + self._implied_options = [] + + # Store all results from _prepare_function + self._prepared_functions = set() + + # Queue of functions to execute, with their arguments + self._execution_queue = [] + + # Store the `when`s associated to some options. + self._conditions = {} + + # A list of conditions to apply as a default `when` for every *_impl() + self._default_conditions = [] + + self._helper = CommandLineHelper(environ, argv) + + assert isinstance(config, dict) + self._config = config + + if logger is None: + logger = moz_logger = logging.getLogger('moz.configure') + logger.setLevel(logging.DEBUG) + formatter = logging.Formatter('%(levelname)s: %(message)s') + handler = ConfigureOutputHandler(stdout, stderr) + handler.setFormatter(formatter) + queue_debug = handler.queue_debug + logger.addHandler(handler) + + else: + assert isinstance(logger, logging.Logger) + moz_logger = None + @contextmanager + def queue_debug(): + yield + + # Some callers will manage to log a bytestring with characters in it + # that can't be converted to ascii. Make our log methods robust to this + # by detecting the encoding that a producer is likely to have used. + encoding = getpreferredencoding() + def wrapped_log_method(logger, key): + method = getattr(logger, key) + if not encoding: + return method + def wrapped(*args, **kwargs): + out_args = [ + arg.decode(encoding) if isinstance(arg, str) else arg + for arg in args + ] + return method(*out_args, **kwargs) + return wrapped + + log_namespace = { + k: wrapped_log_method(logger, k) + for k in ('debug', 'info', 'warning', 'error') + } + log_namespace['queue_debug'] = queue_debug + self.log_impl = ReadOnlyNamespace(**log_namespace) + + self._help = None + self._help_option = self.option_impl('--help', + help='print this message') + self._seen.add(self._help_option) + + self._always = DependsFunction(self, lambda: True, []) + self._never = DependsFunction(self, lambda: False, []) + + if self._value_for(self._help_option): + self._help = HelpFormatter(argv[0]) + self._help.add(self._help_option) + elif moz_logger: + handler = logging.FileHandler('config.log', mode='w', delay=True) + handler.setFormatter(formatter) + logger.addHandler(handler) + + def include_file(self, path): + '''Include one file in the sandbox. Users of this class probably want + + Note: this will execute all template invocations, as well as @depends + functions that depend on '--help', but nothing else. + ''' + + if self._paths: + path = mozpath.join(mozpath.dirname(self._paths[-1]), path) + path = mozpath.normpath(path) + if not mozpath.basedir(path, (mozpath.dirname(self._paths[0]),)): + raise ConfigureError( + 'Cannot include `%s` because it is not in a subdirectory ' + 'of `%s`' % (path, mozpath.dirname(self._paths[0]))) + else: + path = mozpath.realpath(mozpath.abspath(path)) + if path in self._all_paths: + raise ConfigureError( + 'Cannot include `%s` because it was included already.' % path) + self._paths.append(path) + self._all_paths.add(path) + + source = open(path, 'rb').read() + + code = compile(source, path, 'exec') + + exec_(code, self) + + self._paths.pop(-1) + + def run(self, path=None): + '''Executes the given file within the sandbox, as well as everything + pending from any other included file, and ensure the overall + consistency of the executed script(s).''' + if path: + self.include_file(path) + + for option in self._options.itervalues(): + # All options must be referenced by some @depends function + if option not in self._seen: + raise ConfigureError( + 'Option `%s` is not handled ; reference it with a @depends' + % option.option + ) + + self._value_for(option) + + # All implied options should exist. + for implied_option in self._implied_options: + value = self._resolve(implied_option.value, + need_help_dependency=False) + if value is not None: + raise ConfigureError( + '`%s`, emitted from `%s` line %d, is unknown.' + % (implied_option.option, implied_option.caller[1], + implied_option.caller[2])) + + # All options should have been removed (handled) by now. + for arg in self._helper: + without_value = arg.split('=', 1)[0] + raise InvalidOptionError('Unknown option: %s' % without_value) + + # Run the execution queue + for func, args in self._execution_queue: + func(*args) + + if self._help: + with LineIO(self.log_impl.info) as out: + self._help.usage(out) + + def __getitem__(self, key): + impl = '%s_impl' % key + func = getattr(self, impl, None) + if func: + return func + + return super(ConfigureSandbox, self).__getitem__(key) + + def __setitem__(self, key, value): + if (key in self.BUILTINS or key == '__builtins__' or + hasattr(self, '%s_impl' % key)): + raise KeyError('Cannot reassign builtins') + + if inspect.isfunction(value) and value not in self._templates: + value, _ = self._prepare_function(value) + + elif (not isinstance(value, SandboxDependsFunction) and + value not in self._templates and + not (inspect.isclass(value) and issubclass(value, Exception))): + raise KeyError('Cannot assign `%s` because it is neither a ' + '@depends nor a @template' % key) + + return super(ConfigureSandbox, self).__setitem__(key, value) + + def _resolve(self, arg, need_help_dependency=True): + if isinstance(arg, SandboxDependsFunction): + return self._value_for_depends(self._depends[arg], + need_help_dependency) + return arg + + def _value_for(self, obj, need_help_dependency=False): + if isinstance(obj, SandboxDependsFunction): + assert obj in self._depends + return self._value_for_depends(self._depends[obj], + need_help_dependency) + + elif isinstance(obj, DependsFunction): + return self._value_for_depends(obj, need_help_dependency) + + elif isinstance(obj, Option): + return self._value_for_option(obj) + + assert False + + @memoize + def _value_for_depends(self, obj, need_help_dependency=False): + assert not inspect.isgeneratorfunction(obj.func) + return obj.result + + @memoize + def _value_for_option(self, option): + implied = {} + for implied_option in self._implied_options[:]: + if implied_option.name not in (option.name, option.env): + continue + self._implied_options.remove(implied_option) + + if (implied_option.when and + not self._value_for(implied_option.when)): + continue + + value = self._resolve(implied_option.value, + need_help_dependency=False) + + if value is not None: + if isinstance(value, OptionValue): + pass + elif value is True: + value = PositiveOptionValue() + elif value is False or value == (): + value = NegativeOptionValue() + elif isinstance(value, types.StringTypes): + value = PositiveOptionValue((value,)) + elif isinstance(value, tuple): + value = PositiveOptionValue(value) + else: + raise TypeError("Unexpected type: '%s'" + % type(value).__name__) + + opt = value.format(implied_option.option) + self._helper.add(opt, 'implied') + implied[opt] = implied_option + + try: + value, option_string = self._helper.handle(option) + except ConflictingOptionError as e: + reason = implied[e.arg].reason + if isinstance(reason, Option): + reason = self._raw_options.get(reason) or reason.option + reason = reason.split('=', 1)[0] + raise InvalidOptionError( + "'%s' implied by '%s' conflicts with '%s' from the %s" + % (e.arg, reason, e.old_arg, e.old_origin)) + + if option_string: + self._raw_options[option] = option_string + + when = self._conditions.get(option) + if (when and not self._value_for(when, need_help_dependency=True) and + value is not None and value.origin != 'default'): + if value.origin == 'environment': + # The value we return doesn't really matter, because of the + # requirement for @depends to have the same when. + return None + raise InvalidOptionError( + '%s is not available in this configuration' + % option_string.split('=', 1)[0]) + + return value + + def _dependency(self, arg, callee_name, arg_name=None): + if isinstance(arg, types.StringTypes): + prefix, name, values = Option.split_option(arg) + if values != (): + raise ConfigureError("Option must not contain an '='") + if name not in self._options: + raise ConfigureError("'%s' is not a known option. " + "Maybe it's declared too late?" + % arg) + arg = self._options[name] + self._seen.add(arg) + elif isinstance(arg, SandboxDependsFunction): + assert arg in self._depends + arg = self._depends[arg] + else: + raise TypeError( + "Cannot use object of type '%s' as %sargument to %s" + % (type(arg).__name__, '`%s` ' % arg_name if arg_name else '', + callee_name)) + return arg + + def _normalize_when(self, when, callee_name): + if when is True: + when = self._always + elif when is False: + when = self._never + elif when is not None: + when = self._dependency(when, callee_name, 'when') + + if self._default_conditions: + # Create a pseudo @depends function for the combination of all + # default conditions and `when`. + dependencies = [when] if when else [] + dependencies.extend(self._default_conditions) + if len(dependencies) == 1: + return dependencies[0] + return CombinedDependsFunction(self, all, dependencies) + return when + + @contextmanager + def only_when_impl(self, when): + '''Implementation of only_when() + + `only_when` is a context manager that essentially makes calls to + other sandbox functions within the context block ignored. + ''' + when = self._normalize_when(when, 'only_when') + if when and self._default_conditions[-1:] != [when]: + self._default_conditions.append(when) + yield + self._default_conditions.pop() + else: + yield + + def option_impl(self, *args, **kwargs): + '''Implementation of option() + This function creates and returns an Option() object, passing it the + resolved arguments (uses the result of functions when functions are + passed). In most cases, the result of this function is not expected to + be used. + Command line argument/environment variable parsing for this Option is + handled here. + ''' + when = self._normalize_when(kwargs.get('when'), 'option') + args = [self._resolve(arg) for arg in args] + kwargs = {k: self._resolve(v) for k, v in kwargs.iteritems() + if k != 'when'} + option = Option(*args, **kwargs) + if when: + self._conditions[option] = when + if option.name in self._options: + raise ConfigureError('Option `%s` already defined' % option.option) + if option.env in self._options: + raise ConfigureError('Option `%s` already defined' % option.env) + if option.name: + self._options[option.name] = option + if option.env: + self._options[option.env] = option + + if self._help and (when is None or + self._value_for(when, need_help_dependency=True)): + self._help.add(option) + + return option + + def depends_impl(self, *args, **kwargs): + '''Implementation of @depends() + This function is a decorator. It returns a function that subsequently + takes a function and returns a dummy function. The dummy function + identifies the actual function for the sandbox, while preventing + further function calls from within the sandbox. + + @depends() takes a variable number of option strings or dummy function + references. The decorated function is called as soon as the decorator + is called, and the arguments it receives are the OptionValue or + function results corresponding to each of the arguments to @depends. + As an exception, when a HelpFormatter is attached, only functions that + have '--help' in their @depends argument list are called. + + The decorated function is altered to use a different global namespace + for its execution. This different global namespace exposes a limited + set of functions from os.path. + ''' + for k in kwargs: + if k != 'when': + raise TypeError( + "depends_impl() got an unexpected keyword argument '%s'" + % k) + + when = self._normalize_when(kwargs.get('when'), '@depends') + + if not when and not args: + raise ConfigureError('@depends needs at least one argument') + + dependencies = tuple(self._dependency(arg, '@depends') for arg in args) + + conditions = [ + self._conditions[d] + for d in dependencies + if d in self._conditions and isinstance(d, Option) + ] + for c in conditions: + if c != when: + raise ConfigureError('@depends function needs the same `when` ' + 'as options it depends on') + + def decorator(func): + if inspect.isgeneratorfunction(func): + raise ConfigureError( + 'Cannot decorate generator functions with @depends') + func, glob = self._prepare_function(func) + depends = DependsFunction(self, func, dependencies, when=when) + return depends.sandboxed + + return decorator + + def include_impl(self, what, when=None): + '''Implementation of include(). + Allows to include external files for execution in the sandbox. + It is possible to use a @depends function as argument, in which case + the result of the function is the file name to include. This latter + feature is only really meant for --enable-application/--enable-project. + ''' + with self.only_when_impl(when): + what = self._resolve(what) + if what: + if not isinstance(what, types.StringTypes): + raise TypeError("Unexpected type: '%s'" % type(what).__name__) + self.include_file(what) + + def template_impl(self, func): + '''Implementation of @template. + This function is a decorator. Template functions are called + immediately. They are altered so that their global namespace exposes + a limited set of functions from os.path, as well as `depends` and + `option`. + Templates allow to simplify repetitive constructs, or to implement + helper decorators and somesuch. + ''' + template, glob = self._prepare_function(func) + glob.update( + (k[:-len('_impl')], getattr(self, k)) + for k in dir(self) if k.endswith('_impl') and k != 'template_impl' + ) + glob.update((k, v) for k, v in self.iteritems() if k not in glob) + + # Any function argument to the template must be prepared to be sandboxed. + # If the template itself returns a function (in which case, it's very + # likely a decorator), that function must be prepared to be sandboxed as + # well. + def wrap_template(template): + isfunction = inspect.isfunction + + def maybe_prepare_function(obj): + if isfunction(obj): + func, _ = self._prepare_function(obj) + return func + return obj + + # The following function may end up being prepared to be sandboxed, + # so it mustn't depend on anything from the global scope in this + # file. It can however depend on variables from the closure, thus + # maybe_prepare_function and isfunction are declared above to be + # available there. + @wraps(template) + def wrapper(*args, **kwargs): + args = [maybe_prepare_function(arg) for arg in args] + kwargs = {k: maybe_prepare_function(v) + for k, v in kwargs.iteritems()} + ret = template(*args, **kwargs) + if isfunction(ret): + # We can't expect the sandboxed code to think about all the + # details of implementing decorators, so do some of the + # work for them. If the function takes exactly one function + # as argument and returns a function, it must be a + # decorator, so mark the returned function as wrapping the + # function passed in. + if len(args) == 1 and not kwargs and isfunction(args[0]): + ret = wraps(args[0])(ret) + return wrap_template(ret) + return ret + return wrapper + + wrapper = wrap_template(template) + self._templates.add(wrapper) + return wrapper + + RE_MODULE = re.compile('^[a-zA-Z0-9_\.]+$') + + def imports_impl(self, _import, _from=None, _as=None): + '''Implementation of @imports. + This decorator imports the given _import from the given _from module + optionally under a different _as name. + The options correspond to the various forms for the import builtin. + @imports('sys') + @imports(_from='mozpack', _import='path', _as='mozpath') + ''' + for value, required in ( + (_import, True), (_from, False), (_as, False)): + + if not isinstance(value, types.StringTypes) and ( + required or value is not None): + raise TypeError("Unexpected type: '%s'" % type(value).__name__) + if value is not None and not self.RE_MODULE.match(value): + raise ValueError("Invalid argument to @imports: '%s'" % value) + if _as and '.' in _as: + raise ValueError("Invalid argument to @imports: '%s'" % _as) + + def decorator(func): + if func in self._templates: + raise ConfigureError( + '@imports must appear after @template') + if func in self._depends: + raise ConfigureError( + '@imports must appear after @depends') + # For the imports to apply in the order they appear in the + # .configure file, we accumulate them in reverse order and apply + # them later. + imports = self._imports.setdefault(func, []) + imports.insert(0, (_from, _import, _as)) + return func + + return decorator + + def _apply_imports(self, func, glob): + for _from, _import, _as in self._imports.get(func, ()): + _from = '%s.' % _from if _from else '' + if _as: + glob[_as] = self._get_one_import('%s%s' % (_from, _import)) + else: + what = _import.split('.')[0] + glob[what] = self._get_one_import('%s%s' % (_from, what)) + + def _get_one_import(self, what): + # The special `__sandbox__` module gives access to the sandbox + # instance. + if what == '__sandbox__': + return self + # Special case for the open() builtin, because otherwise, using it + # fails with "IOError: file() constructor not accessible in + # restricted mode" + if what == '__builtin__.open': + return lambda *args, **kwargs: open(*args, **kwargs) + # Until this proves to be a performance problem, just construct an + # import statement and execute it. + import_line = '' + if '.' in what: + _from, what = what.rsplit('.', 1) + import_line += 'from %s ' % _from + import_line += 'import %s as imported' % what + glob = {} + exec_(import_line, {}, glob) + return glob['imported'] + + def _resolve_and_set(self, data, name, value, when=None): + # Don't set anything when --help was on the command line + if self._help: + return + if when and not self._value_for(when): + return + name = self._resolve(name, need_help_dependency=False) + if name is None: + return + if not isinstance(name, types.StringTypes): + raise TypeError("Unexpected type: '%s'" % type(name).__name__) + if name in data: + raise ConfigureError( + "Cannot add '%s' to configuration: Key already " + "exists" % name) + value = self._resolve(value, need_help_dependency=False) + if value is not None: + data[name] = value + + def set_config_impl(self, name, value, when=None): + '''Implementation of set_config(). + Set the configuration items with the given name to the given value. + Both `name` and `value` can be references to @depends functions, + in which case the result from these functions is used. If the result + of either function is None, the configuration item is not set. + ''' + when = self._normalize_when(when, 'set_config') + + self._execution_queue.append(( + self._resolve_and_set, (self._config, name, value, when))) + + def set_define_impl(self, name, value, when=None): + '''Implementation of set_define(). + Set the define with the given name to the given value. Both `name` and + `value` can be references to @depends functions, in which case the + result from these functions is used. If the result of either function + is None, the define is not set. If the result is False, the define is + explicitly undefined (-U). + ''' + when = self._normalize_when(when, 'set_define') + + defines = self._config.setdefault('DEFINES', {}) + self._execution_queue.append(( + self._resolve_and_set, (defines, name, value, when))) + + def imply_option_impl(self, option, value, reason=None, when=None): + '''Implementation of imply_option(). + Injects additional options as if they had been passed on the command + line. The `option` argument is a string as in option()'s `name` or + `env`. The option must be declared after `imply_option` references it. + The `value` argument indicates the value to pass to the option. + It can be: + - True. In this case `imply_option` injects the positive option + (--enable-foo/--with-foo). + imply_option('--enable-foo', True) + imply_option('--disable-foo', True) + are both equivalent to `--enable-foo` on the command line. + + - False. In this case `imply_option` injects the negative option + (--disable-foo/--without-foo). + imply_option('--enable-foo', False) + imply_option('--disable-foo', False) + are both equivalent to `--disable-foo` on the command line. + + - None. In this case `imply_option` does nothing. + imply_option('--enable-foo', None) + imply_option('--disable-foo', None) + are both equivalent to not passing any flag on the command line. + + - a string or a tuple. In this case `imply_option` injects the positive + option with the given value(s). + imply_option('--enable-foo', 'a') + imply_option('--disable-foo', 'a') + are both equivalent to `--enable-foo=a` on the command line. + imply_option('--enable-foo', ('a', 'b')) + imply_option('--disable-foo', ('a', 'b')) + are both equivalent to `--enable-foo=a,b` on the command line. + + Because imply_option('--disable-foo', ...) can be misleading, it is + recommended to use the positive form ('--enable' or '--with') for + `option`. + + The `value` argument can also be (and usually is) a reference to a + @depends function, in which case the result of that function will be + used as per the descripted mapping above. + + The `reason` argument indicates what caused the option to be implied. + It is necessary when it cannot be inferred from the `value`. + ''' + # Don't do anything when --help was on the command line + if self._help: + return + if not reason and isinstance(value, SandboxDependsFunction): + deps = self._depends[value].dependencies + possible_reasons = [d for d in deps if d != self._help_option] + if len(possible_reasons) == 1: + if isinstance(possible_reasons[0], Option): + reason = possible_reasons[0] + if not reason and (isinstance(value, (bool, tuple)) or + isinstance(value, types.StringTypes)): + # A reason can be provided automatically when imply_option + # is called with an immediate value. + _, filename, line, _, _, _ = inspect.stack()[1] + reason = "imply_option at %s:%s" % (filename, line) + + if not reason: + raise ConfigureError( + "Cannot infer what implies '%s'. Please add a `reason` to " + "the `imply_option` call." + % option) + + when = self._normalize_when(when, 'imply_option') + + prefix, name, values = Option.split_option(option) + if values != (): + raise ConfigureError("Implied option must not contain an '='") + + self._implied_options.append(ReadOnlyNamespace( + option=option, + prefix=prefix, + name=name, + value=value, + caller=inspect.stack()[1], + reason=reason, + when=when, + )) + + def _prepare_function(self, func): + '''Alter the given function global namespace with the common ground + for @depends, and @template. + ''' + if not inspect.isfunction(func): + raise TypeError("Unexpected type: '%s'" % type(func).__name__) + if func in self._prepared_functions: + return func, func.func_globals + + glob = SandboxedGlobal( + (k, v) for k, v in func.func_globals.iteritems() + if (inspect.isfunction(v) and v not in self._templates) or ( + inspect.isclass(v) and issubclass(v, Exception)) + ) + glob.update( + __builtins__=self.BUILTINS, + __file__=self._paths[-1] if self._paths else '', + __name__=self._paths[-1] if self._paths else '', + os=self.OS, + log=self.log_impl, + ) + + # The execution model in the sandbox doesn't guarantee the execution + # order will always be the same for a given function, and if it uses + # variables from a closure that are changed after the function is + # declared, depending when the function is executed, the value of the + # variable can differ. For consistency, we force the function to use + # the value from the earliest it can be run, which is at declaration. + # Note this is not entirely bullet proof (if the value is e.g. a list, + # the list contents could have changed), but covers the bases. + closure = None + if func.func_closure: + def makecell(content): + def f(): + content + return f.func_closure[0] + + closure = tuple(makecell(cell.cell_contents) + for cell in func.func_closure) + + new_func = wraps(func)(types.FunctionType( + func.func_code, + glob, + func.__name__, + func.func_defaults, + closure + )) + @wraps(new_func) + def wrapped(*args, **kwargs): + if func in self._imports: + self._apply_imports(func, glob) + del self._imports[func] + return new_func(*args, **kwargs) + + self._prepared_functions.add(wrapped) + return wrapped, glob diff --git a/python/mozbuild/mozbuild/configure/check_debug_ranges.py b/python/mozbuild/mozbuild/configure/check_debug_ranges.py new file mode 100644 index 000000000..ca312dff4 --- /dev/null +++ b/python/mozbuild/mozbuild/configure/check_debug_ranges.py @@ -0,0 +1,62 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# This script returns the number of items for the DW_AT_ranges corresponding +# to a given compilation unit. This is used as a helper to find a bug in some +# versions of GNU ld. + +from __future__ import absolute_import + +import subprocess +import sys +import re + +def get_range_for(compilation_unit, debug_info): + '''Returns the range offset for a given compilation unit + in a given debug_info.''' + name = ranges = '' + search_cu = False + for nfo in debug_info.splitlines(): + if 'DW_TAG_compile_unit' in nfo: + search_cu = True + elif 'DW_TAG_' in nfo or not nfo.strip(): + if name == compilation_unit and ranges != '': + return int(ranges, 16) + name = ranges = '' + search_cu = False + if search_cu: + if 'DW_AT_name' in nfo: + name = nfo.rsplit(None, 1)[1] + elif 'DW_AT_ranges' in nfo: + ranges = nfo.rsplit(None, 1)[1] + return None + +def get_range_length(range, debug_ranges): + '''Returns the number of items in the range starting at the + given offset.''' + length = 0 + for line in debug_ranges.splitlines(): + m = re.match('\s*([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+([0-9a-fA-F]+)', line) + if m and int(m.group(1), 16) == range: + length += 1 + return length + +def main(bin, compilation_unit): + p = subprocess.Popen(['objdump', '-W', bin], stdout = subprocess.PIPE, stderr = subprocess.PIPE) + (out, err) = p.communicate() + sections = re.split('\n(Contents of the|The section) ', out) + debug_info = [s for s in sections if s.startswith('.debug_info')] + debug_ranges = [s for s in sections if s.startswith('.debug_ranges')] + if not debug_ranges or not debug_info: + return 0 + + range = get_range_for(compilation_unit, debug_info[0]) + if range is not None: + return get_range_length(range, debug_ranges[0]) + + return -1 + + +if __name__ == '__main__': + print main(*sys.argv[1:]) diff --git a/python/mozbuild/mozbuild/configure/constants.py b/python/mozbuild/mozbuild/configure/constants.py new file mode 100644 index 000000000..dfc7cf8ad --- /dev/null +++ b/python/mozbuild/mozbuild/configure/constants.py @@ -0,0 +1,103 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from __future__ import absolute_import, print_function, unicode_literals + +from mozbuild.util import EnumString +from collections import OrderedDict + + +CompilerType = EnumString.subclass( + 'clang', + 'clang-cl', + 'gcc', + 'msvc', +) + +OS = EnumString.subclass( + 'Android', + 'DragonFly', + 'FreeBSD', + 'GNU', + 'iOS', + 'NetBSD', + 'OpenBSD', + 'OSX', + 'WINNT', +) + +Kernel = EnumString.subclass( + 'Darwin', + 'DragonFly', + 'FreeBSD', + 'kFreeBSD', + 'Linux', + 'NetBSD', + 'OpenBSD', + 'WINNT', +) + +CPU_bitness = { + 'aarch64': 64, + 'Alpha': 32, + 'arm': 32, + 'hppa': 32, + 'ia64': 64, + 'mips32': 32, + 'mips64': 64, + 'ppc': 32, + 'ppc64': 64, + 's390': 32, + 's390x': 64, + 'sparc': 32, + 'sparc64': 64, + 'x86': 32, + 'x86_64': 64, +} + +CPU = EnumString.subclass(*CPU_bitness.keys()) + +Endianness = EnumString.subclass( + 'big', + 'little', +) + +WindowsBinaryType = EnumString.subclass( + 'win32', + 'win64', +) + +# The order of those checks matter +CPU_preprocessor_checks = OrderedDict(( + ('x86', '__i386__ || _M_IX86'), + ('x86_64', '__x86_64__ || _M_X64'), + ('arm', '__arm__ || _M_ARM'), + ('aarch64', '__aarch64__'), + ('ia64', '__ia64__'), + ('s390x', '__s390x__'), + ('s390', '__s390__'), + ('ppc64', '__powerpc64__'), + ('ppc', '__powerpc__'), + ('Alpha', '__alpha__'), + ('hppa', '__hppa__'), + ('sparc64', '__sparc__ && __arch64__'), + ('sparc', '__sparc__'), + ('mips64', '__mips64'), + ('mips32', '__mips__'), +)) + +assert sorted(CPU_preprocessor_checks.keys()) == sorted(CPU.POSSIBLE_VALUES) + +kernel_preprocessor_checks = { + 'Darwin': '__APPLE__', + 'DragonFly': '__DragonFly__', + 'FreeBSD': '__FreeBSD__', + 'kFreeBSD': '__FreeBSD_kernel__', + 'Linux': '__linux__', + 'NetBSD': '__NetBSD__', + 'OpenBSD': '__OpenBSD__', + 'WINNT': '_WIN32 || __CYGWIN__', +} + +assert sorted(kernel_preprocessor_checks.keys()) == sorted(Kernel.POSSIBLE_VALUES) diff --git a/python/mozbuild/mozbuild/configure/help.py b/python/mozbuild/mozbuild/configure/help.py new file mode 100644 index 000000000..cd7876fbd --- /dev/null +++ b/python/mozbuild/mozbuild/configure/help.py @@ -0,0 +1,45 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from __future__ import absolute_import, print_function, unicode_literals + +import os +from mozbuild.configure.options import Option + + +class HelpFormatter(object): + def __init__(self, argv0): + self.intro = ['Usage: %s [options]' % os.path.basename(argv0)] + self.options = ['Options: [defaults in brackets after descriptions]'] + self.env = ['Environment variables:'] + + def add(self, option): + assert isinstance(option, Option) + + if option.possible_origins == ('implied',): + # Don't display help if our option can only be implied. + return + + # TODO: improve formatting + target = self.options if option.name else self.env + opt = option.option + if option.choices: + opt += '={%s}' % ','.join(option.choices) + help = option.help or '' + if len(option.default): + if help: + help += ' ' + help += '[%s]' % ','.join(option.default) + + if len(opt) > 24 or not help: + target.append(' %s' % opt) + if help: + target.append('%s%s' % (' ' * 28, help)) + else: + target.append(' %-24s %s' % (opt, help)) + + def usage(self, out): + print('\n\n'.join('\n'.join(t) + for t in (self.intro, self.options, self.env)), + file=out) diff --git a/python/mozbuild/mozbuild/configure/libstdcxx.py b/python/mozbuild/mozbuild/configure/libstdcxx.py new file mode 100644 index 000000000..cab0ccb11 --- /dev/null +++ b/python/mozbuild/mozbuild/configure/libstdcxx.py @@ -0,0 +1,81 @@ +#!/usr/bin/python +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +# This script find the version of libstdc++ and prints it as single number +# with 8 bits per element. For example, GLIBCXX_3.4.10 becomes +# 3 << 16 | 4 << 8 | 10 = 197642. This format is easy to use +# in the C preprocessor. + +# We find out both the host and target versions. Since the output +# will be used from shell, we just print the two assignments and evaluate +# them from shell. + +from __future__ import absolute_import + +import os +import subprocess +import re + +re_for_ld = re.compile('.*\((.*)\).*') + +def parse_readelf_line(x): + """Return the version from a readelf line that looks like: + 0x00ec: Rev: 1 Flags: none Index: 8 Cnt: 2 Name: GLIBCXX_3.4.6 + """ + return x.split(':')[-1].split('_')[-1].strip() + +def parse_ld_line(x): + """Parse a line from the output of ld -t. The output of gold is just + the full path, gnu ld prints "-lstdc++ (path)". + """ + t = re_for_ld.match(x) + if t: + return t.groups()[0].strip() + return x.strip() + +def split_ver(v): + """Covert the string '1.2.3' into the list [1,2,3] + """ + return [int(x) for x in v.split('.')] + +def cmp_ver(a, b): + """Compare versions in the form 'a.b.c' + """ + for (i, j) in zip(split_ver(a), split_ver(b)): + if i != j: + return i - j + return 0 + +def encode_ver(v): + """Encode the version as a single number. + """ + t = split_ver(v) + return t[0] << 16 | t[1] << 8 | t[2] + +def find_version(e): + """Given the value of environment variable CXX or HOST_CXX, find the + version of the libstdc++ it uses. + """ + args = e.split() + args += ['-shared', '-Wl,-t'] + p = subprocess.Popen(args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE) + candidates = [x for x in p.stdout if 'libstdc++.so' in x] + if not candidates: + return '' + assert len(candidates) == 1 + libstdcxx = parse_ld_line(candidates[-1]) + + p = subprocess.Popen(['readelf', '-V', libstdcxx], stdout=subprocess.PIPE) + versions = [parse_readelf_line(x) + for x in p.stdout.readlines() if 'Name: GLIBCXX' in x] + last_version = sorted(versions, cmp = cmp_ver)[-1] + return encode_ver(last_version) + +if __name__ == '__main__': + cxx_env = os.environ['CXX'] + print 'MOZ_LIBSTDCXX_TARGET_VERSION=%s' % find_version(cxx_env) + host_cxx_env = os.environ.get('HOST_CXX', cxx_env) + print 'MOZ_LIBSTDCXX_HOST_VERSION=%s' % find_version(host_cxx_env) diff --git a/python/mozbuild/mozbuild/configure/lint.py b/python/mozbuild/mozbuild/configure/lint.py new file mode 100644 index 000000000..e0a5c8328 --- /dev/null +++ b/python/mozbuild/mozbuild/configure/lint.py @@ -0,0 +1,78 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from __future__ import absolute_import, print_function, unicode_literals + +from StringIO import StringIO +from . import ( + CombinedDependsFunction, + ConfigureError, + ConfigureSandbox, + DependsFunction, +) +from .lint_util import disassemble_as_iter +from mozbuild.util import memoize + + +class LintSandbox(ConfigureSandbox): + def __init__(self, environ=None, argv=None, stdout=None, stderr=None): + out = StringIO() + stdout = stdout or out + stderr = stderr or out + environ = environ or {} + argv = argv or [] + self._wrapped = {} + super(LintSandbox, self).__init__({}, environ=environ, argv=argv, + stdout=stdout, stderr=stderr) + + def run(self, path=None): + if path: + self.include_file(path) + + def _missing_help_dependency(self, obj): + if isinstance(obj, CombinedDependsFunction): + return False + if isinstance(obj, DependsFunction): + if (self._help_option in obj.dependencies or + obj in (self._always, self._never)): + return False + func, glob = self._wrapped[obj.func] + # We allow missing --help dependencies for functions that: + # - don't use @imports + # - don't have a closure + # - don't use global variables + if func in self._imports or func.func_closure: + return True + for op, arg in disassemble_as_iter(func): + if op in ('LOAD_GLOBAL', 'STORE_GLOBAL'): + # There is a fake os module when one is not imported, + # and it's allowed for functions without a --help + # dependency. + if arg == 'os' and glob.get('os') is self.OS: + continue + return True + return False + + @memoize + def _value_for_depends(self, obj, need_help_dependency=False): + with_help = self._help_option in obj.dependencies + if with_help: + for arg in obj.dependencies: + if self._missing_help_dependency(arg): + raise ConfigureError( + "`%s` depends on '--help' and `%s`. " + "`%s` must depend on '--help'" + % (obj.name, arg.name, arg.name)) + elif ((self._help or need_help_dependency) and + self._missing_help_dependency(obj)): + raise ConfigureError("Missing @depends for `%s`: '--help'" % + obj.name) + return super(LintSandbox, self)._value_for_depends( + obj, need_help_dependency) + + def _prepare_function(self, func): + wrapped, glob = super(LintSandbox, self)._prepare_function(func) + if wrapped not in self._wrapped: + self._wrapped[wrapped] = func, glob + return wrapped, glob diff --git a/python/mozbuild/mozbuild/configure/lint_util.py b/python/mozbuild/mozbuild/configure/lint_util.py new file mode 100644 index 000000000..f1c2f8731 --- /dev/null +++ b/python/mozbuild/mozbuild/configure/lint_util.py @@ -0,0 +1,52 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from __future__ import absolute_import, print_function, unicode_literals + +import dis +import inspect + + +# dis.dis only outputs to stdout. This is a modified version that +# returns an iterator. +def disassemble_as_iter(co): + if inspect.ismethod(co): + co = co.im_func + if inspect.isfunction(co): + co = co.func_code + code = co.co_code + n = len(code) + i = 0 + extended_arg = 0 + free = None + while i < n: + c = code[i] + op = ord(c) + opname = dis.opname[op] + i += 1; + if op >= dis.HAVE_ARGUMENT: + arg = ord(code[i]) + ord(code[i + 1]) * 256 + extended_arg + extended_arg = 0 + i += 2 + if op == dis.EXTENDED_ARG: + extended_arg = arg * 65536L + continue + if op in dis.hasconst: + yield opname, co.co_consts[arg] + elif op in dis.hasname: + yield opname, co.co_names[arg] + elif op in dis.hasjrel: + yield opname, i + arg + elif op in dis.haslocal: + yield opname, co.co_varnames[arg] + elif op in dis.hascompare: + yield opname, dis.cmp_op[arg] + elif op in dis.hasfree: + if free is None: + free = co.co_cellvars + co.co_freevars + yield opname, free[arg] + else: + yield opname, None + else: + yield opname, None diff --git a/python/mozbuild/mozbuild/configure/options.py b/python/mozbuild/mozbuild/configure/options.py new file mode 100644 index 000000000..4310c8627 --- /dev/null +++ b/python/mozbuild/mozbuild/configure/options.py @@ -0,0 +1,485 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from __future__ import absolute_import, print_function, unicode_literals + +import os +import sys +import types +from collections import OrderedDict + + +def istupleofstrings(obj): + return isinstance(obj, tuple) and len(obj) and all( + isinstance(o, types.StringTypes) for o in obj) + + +class OptionValue(tuple): + '''Represents the value of a configure option. + + This class is not meant to be used directly. Use its subclasses instead. + + The `origin` attribute holds where the option comes from (e.g. environment, + command line, or default) + ''' + def __new__(cls, values=(), origin='unknown'): + return super(OptionValue, cls).__new__(cls, values) + + def __init__(self, values=(), origin='unknown'): + self.origin = origin + + def format(self, option): + if option.startswith('--'): + prefix, name, values = Option.split_option(option) + assert values == () + for prefix_set in ( + ('disable', 'enable'), + ('without', 'with'), + ): + if prefix in prefix_set: + prefix = prefix_set[int(bool(self))] + break + if prefix: + option = '--%s-%s' % (prefix, name) + elif self: + option = '--%s' % name + else: + return '' + if len(self): + return '%s=%s' % (option, ','.join(self)) + return option + elif self and not len(self): + return '%s=1' % option + return '%s=%s' % (option, ','.join(self)) + + def __eq__(self, other): + if type(other) != type(self): + return False + return super(OptionValue, self).__eq__(other) + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return '%s%s' % (self.__class__.__name__, + super(OptionValue, self).__repr__()) + + +class PositiveOptionValue(OptionValue): + '''Represents the value for a positive option (--enable/--with/--foo) + in the form of a tuple for when values are given to the option (in the form + --option=value[,value2...]. + ''' + def __nonzero__(self): + return True + + +class NegativeOptionValue(OptionValue): + '''Represents the value for a negative option (--disable/--without) + + This is effectively an empty tuple with a `origin` attribute. + ''' + def __new__(cls, origin='unknown'): + return super(NegativeOptionValue, cls).__new__(cls, origin=origin) + + def __init__(self, origin='unknown'): + return super(NegativeOptionValue, self).__init__(origin=origin) + + +class InvalidOptionError(Exception): + pass + + +class ConflictingOptionError(InvalidOptionError): + def __init__(self, message, **format_data): + if format_data: + message = message.format(**format_data) + super(ConflictingOptionError, self).__init__(message) + for k, v in format_data.iteritems(): + setattr(self, k, v) + + +class Option(object): + '''Represents a configure option + + A configure option can be a command line flag or an environment variable + or both. + + - `name` is the full command line flag (e.g. --enable-foo). + - `env` is the environment variable name (e.g. ENV) + - `nargs` is the number of arguments the option may take. It can be a + number or the special values '?' (0 or 1), '*' (0 or more), or '+' (1 or + more). + - `default` can be used to give a default value to the option. When the + `name` of the option starts with '--enable-' or '--with-', the implied + default is an empty PositiveOptionValue. When it starts with '--disable-' + or '--without-', the implied default is a NegativeOptionValue. + - `choices` restricts the set of values that can be given to the option. + - `help` is the option description for use in the --help output. + - `possible_origins` is a tuple of strings that are origins accepted for + this option. Example origins are 'mozconfig', 'implied', and 'environment'. + ''' + __slots__ = ( + 'id', 'prefix', 'name', 'env', 'nargs', 'default', 'choices', 'help', + 'possible_origins', + ) + + def __init__(self, name=None, env=None, nargs=None, default=None, + possible_origins=None, choices=None, help=None): + if not name and not env: + raise InvalidOptionError( + 'At least an option name or an environment variable name must ' + 'be given') + if name: + if not isinstance(name, types.StringTypes): + raise InvalidOptionError('Option must be a string') + if not name.startswith('--'): + raise InvalidOptionError('Option must start with `--`') + if '=' in name: + raise InvalidOptionError('Option must not contain an `=`') + if not name.islower(): + raise InvalidOptionError('Option must be all lowercase') + if env: + if not isinstance(env, types.StringTypes): + raise InvalidOptionError( + 'Environment variable name must be a string') + if not env.isupper(): + raise InvalidOptionError( + 'Environment variable name must be all uppercase') + if nargs not in (None, '?', '*', '+') and not ( + isinstance(nargs, int) and nargs >= 0): + raise InvalidOptionError( + "nargs must be a positive integer, '?', '*' or '+'") + if (not isinstance(default, types.StringTypes) and + not isinstance(default, (bool, types.NoneType)) and + not istupleofstrings(default)): + raise InvalidOptionError( + 'default must be a bool, a string or a tuple of strings') + if choices and not istupleofstrings(choices): + raise InvalidOptionError( + 'choices must be a tuple of strings') + if not help: + raise InvalidOptionError('A help string must be provided') + if possible_origins and not istupleofstrings(possible_origins): + raise InvalidOptionError( + 'possible_origins must be a tuple of strings') + self.possible_origins = possible_origins + + if name: + prefix, name, values = self.split_option(name) + assert values == () + + # --disable and --without options mean the default is enabled. + # --enable and --with options mean the default is disabled. + # However, we allow a default to be given so that the default + # can be affected by other factors. + if prefix: + if default is None: + default = prefix in ('disable', 'without') + elif default is False: + prefix = { + 'disable': 'enable', + 'without': 'with', + }.get(prefix, prefix) + elif default is True: + prefix = { + 'enable': 'disable', + 'with': 'without', + }.get(prefix, prefix) + else: + prefix = '' + + self.prefix = prefix + self.name = name + self.env = env + if default in (None, False): + self.default = NegativeOptionValue(origin='default') + elif isinstance(default, tuple): + self.default = PositiveOptionValue(default, origin='default') + elif default is True: + self.default = PositiveOptionValue(origin='default') + else: + self.default = PositiveOptionValue((default,), origin='default') + if nargs is None: + nargs = 0 + if len(self.default) == 1: + nargs = '?' + elif len(self.default) > 1: + nargs = '*' + elif choices: + nargs = 1 + self.nargs = nargs + has_choices = choices is not None + if isinstance(self.default, PositiveOptionValue): + if has_choices and len(self.default) == 0: + raise InvalidOptionError( + 'A `default` must be given along with `choices`') + if not self._validate_nargs(len(self.default)): + raise InvalidOptionError( + "The given `default` doesn't satisfy `nargs`") + if has_choices and not all(d in choices for d in self.default): + raise InvalidOptionError( + 'The `default` value must be one of %s' % + ', '.join("'%s'" % c for c in choices)) + elif has_choices: + maxargs = self.maxargs + if len(choices) < maxargs and maxargs != sys.maxint: + raise InvalidOptionError('Not enough `choices` for `nargs`') + self.choices = choices + self.help = help + + @staticmethod + def split_option(option): + '''Split a flag or variable into a prefix, a name and values + + Variables come in the form NAME=values (no prefix). + Flags come in the form --name=values or --prefix-name=values + where prefix is one of 'with', 'without', 'enable' or 'disable'. + The '=values' part is optional. Values are separated with commas. + ''' + if not isinstance(option, types.StringTypes): + raise InvalidOptionError('Option must be a string') + + elements = option.split('=', 1) + name = elements[0] + values = tuple(elements[1].split(',')) if len(elements) == 2 else () + if name.startswith('--'): + name = name[2:] + if not name.islower(): + raise InvalidOptionError('Option must be all lowercase') + elements = name.split('-', 1) + prefix = elements[0] + if len(elements) == 2 and prefix in ('enable', 'disable', + 'with', 'without'): + return prefix, elements[1], values + else: + if name.startswith('-'): + raise InvalidOptionError( + 'Option must start with two dashes instead of one') + if name.islower(): + raise InvalidOptionError( + 'Environment variable name must be all uppercase') + return '', name, values + + @staticmethod + def _join_option(prefix, name): + # The constraints around name and env in __init__ make it so that + # we can distinguish between flags and environment variables with + # islower/isupper. + if name.isupper(): + assert not prefix + return name + elif prefix: + return '--%s-%s' % (prefix, name) + return '--%s' % name + + @property + def option(self): + if self.prefix or self.name: + return self._join_option(self.prefix, self.name) + else: + return self.env + + @property + def minargs(self): + if isinstance(self.nargs, int): + return self.nargs + return 1 if self.nargs == '+' else 0 + + @property + def maxargs(self): + if isinstance(self.nargs, int): + return self.nargs + return 1 if self.nargs == '?' else sys.maxint + + def _validate_nargs(self, num): + minargs, maxargs = self.minargs, self.maxargs + return num >= minargs and num <= maxargs + + def get_value(self, option=None, origin='unknown'): + '''Given a full command line option (e.g. --enable-foo=bar) or a + variable assignment (FOO=bar), returns the corresponding OptionValue. + + Note: variable assignments can come from either the environment or + from the command line (e.g. `../configure CFLAGS=-O2`) + ''' + if not option: + return self.default + + if self.possible_origins and origin not in self.possible_origins: + raise InvalidOptionError( + '%s can not be set by %s. Values are accepted from: %s' % + (option, origin, ', '.join(self.possible_origins))) + + prefix, name, values = self.split_option(option) + option = self._join_option(prefix, name) + + assert name in (self.name, self.env) + + if prefix in ('disable', 'without'): + if values != (): + raise InvalidOptionError('Cannot pass a value to %s' % option) + return NegativeOptionValue(origin=origin) + + if name == self.env: + if values == ('',): + return NegativeOptionValue(origin=origin) + if self.nargs in (0, '?', '*') and values == ('1',): + return PositiveOptionValue(origin=origin) + + values = PositiveOptionValue(values, origin=origin) + + if not self._validate_nargs(len(values)): + raise InvalidOptionError('%s takes %s value%s' % ( + option, + { + '?': '0 or 1', + '*': '0 or more', + '+': '1 or more', + }.get(self.nargs, str(self.nargs)), + 's' if (not isinstance(self.nargs, int) or + self.nargs != 1) else '' + )) + + if len(values) and self.choices: + relative_result = None + for val in values: + if self.nargs in ('+', '*'): + if val.startswith(('+', '-')): + if relative_result is None: + relative_result = list(self.default) + sign = val[0] + val = val[1:] + if sign == '+': + if val not in relative_result: + relative_result.append(val) + else: + try: + relative_result.remove(val) + except ValueError: + pass + + if val not in self.choices: + raise InvalidOptionError( + "'%s' is not one of %s" + % (val, ', '.join("'%s'" % c for c in self.choices))) + + if relative_result is not None: + values = PositiveOptionValue(relative_result, origin=origin) + + return values + + def __repr__(self): + return '<%s.%s [%s]>' % (self.__class__.__module__, + self.__class__.__name__, self.option) + + +class CommandLineHelper(object): + '''Helper class to handle the various ways options can be given either + on the command line of through the environment. + + For instance, an Option('--foo', env='FOO') can be passed as --foo on the + command line, or as FOO=1 in the environment *or* on the command line. + + If multiple variants are given, command line is prefered over the + environment, and if different values are given on the command line, the + last one wins. (This mimicks the behavior of autoconf, avoiding to break + existing mozconfigs using valid options in weird ways) + + Extra options can be added afterwards through API calls. For those, + conflicting values will raise an exception. + ''' + def __init__(self, environ=os.environ, argv=sys.argv): + self._environ = dict(environ) + self._args = OrderedDict() + self._extra_args = OrderedDict() + self._origins = {} + self._last = 0 + + for arg in argv[1:]: + self.add(arg, 'command-line', self._args) + + def add(self, arg, origin='command-line', args=None): + assert origin != 'default' + prefix, name, values = Option.split_option(arg) + if args is None: + args = self._extra_args + if args is self._extra_args and name in self._extra_args: + old_arg = self._extra_args[name][0] + old_prefix, _, old_values = Option.split_option(old_arg) + if prefix != old_prefix or values != old_values: + raise ConflictingOptionError( + "Cannot add '{arg}' to the {origin} set because it " + "conflicts with '{old_arg}' that was added earlier", + arg=arg, origin=origin, old_arg=old_arg, + old_origin=self._origins[old_arg]) + self._last += 1 + args[name] = arg, self._last + self._origins[arg] = origin + + def _prepare(self, option, args): + arg = None + origin = 'command-line' + from_name = args.get(option.name) + from_env = args.get(option.env) + if from_name and from_env: + arg1, pos1 = from_name + arg2, pos2 = from_env + arg, pos = (arg1, pos1) if abs(pos1) > abs(pos2) else (arg2, pos2) + if args is self._extra_args and (option.get_value(arg1) != + option.get_value(arg2)): + origin = self._origins[arg] + old_arg = arg2 if abs(pos1) > abs(pos2) else arg1 + raise ConflictingOptionError( + "Cannot add '{arg}' to the {origin} set because it " + "conflicts with '{old_arg}' that was added earlier", + arg=arg, origin=origin, old_arg=old_arg, + old_origin=self._origins[old_arg]) + elif from_name or from_env: + arg, pos = from_name if from_name else from_env + elif option.env and args is self._args: + env = self._environ.get(option.env) + if env is not None: + arg = '%s=%s' % (option.env, env) + origin = 'environment' + + origin = self._origins.get(arg, origin) + + for k in (option.name, option.env): + try: + del args[k] + except KeyError: + pass + + return arg, origin + + def handle(self, option): + '''Return the OptionValue corresponding to the given Option instance, + depending on the command line, environment, and extra arguments, and + the actual option or variable that set it. + Only works once for a given Option. + ''' + assert isinstance(option, Option) + + arg, origin = self._prepare(option, self._args) + ret = option.get_value(arg, origin) + + extra_arg, extra_origin = self._prepare(option, self._extra_args) + extra_ret = option.get_value(extra_arg, extra_origin) + + if extra_ret.origin == 'default': + return ret, arg + + if ret.origin != 'default' and extra_ret != ret: + raise ConflictingOptionError( + "Cannot add '{arg}' to the {origin} set because it conflicts " + "with {old_arg} from the {old_origin} set", arg=extra_arg, + origin=extra_ret.origin, old_arg=arg, old_origin=ret.origin) + + return extra_ret, extra_arg + + def __iter__(self): + for d in (self._args, self._extra_args): + for arg, pos in d.itervalues(): + yield arg diff --git a/python/mozbuild/mozbuild/configure/util.py b/python/mozbuild/mozbuild/configure/util.py new file mode 100644 index 000000000..c7a305282 --- /dev/null +++ b/python/mozbuild/mozbuild/configure/util.py @@ -0,0 +1,226 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from __future__ import absolute_import, print_function, unicode_literals + +import codecs +import itertools +import locale +import logging +import os +import sys +from collections import deque +from contextlib import contextmanager +from distutils.version import LooseVersion + +def getpreferredencoding(): + # locale._parse_localename makes locale.getpreferredencoding + # return None when LC_ALL is C, instead of e.g. 'US-ASCII' or + # 'ANSI_X3.4-1968' when it uses nl_langinfo. + encoding = None + try: + encoding = locale.getpreferredencoding() + except ValueError: + # On english OSX, LC_ALL is UTF-8 (not en-US.UTF-8), and + # that throws off locale._parse_localename, which ends up + # being used on e.g. homebrew python. + if os.environ.get('LC_ALL', '').upper() == 'UTF-8': + encoding = 'utf-8' + return encoding + +class Version(LooseVersion): + '''A simple subclass of distutils.version.LooseVersion. + Adds attributes for `major`, `minor`, `patch` for the first three + version components so users can easily pull out major/minor + versions, like: + + v = Version('1.2b') + v.major == 1 + v.minor == 2 + v.patch == 0 + ''' + def __init__(self, version): + # Can't use super, LooseVersion's base class is not a new-style class. + LooseVersion.__init__(self, version) + # Take the first three integer components, stopping at the first + # non-integer and padding the rest with zeroes. + (self.major, self.minor, self.patch) = list(itertools.chain( + itertools.takewhile(lambda x:isinstance(x, int), self.version), + (0, 0, 0)))[:3] + + + def __cmp__(self, other): + # LooseVersion checks isinstance(StringType), so work around it. + if isinstance(other, unicode): + other = other.encode('ascii') + return LooseVersion.__cmp__(self, other) + + +class ConfigureOutputHandler(logging.Handler): + '''A logging handler class that sends info messages to stdout and other + messages to stderr. + + Messages sent to stdout are not formatted with the attached Formatter. + Additionally, if they end with '... ', no newline character is printed, + making the next message printed follow the '... '. + + Only messages above log level INFO (included) are logged. + + Messages below that level can be kept until an ERROR message is received, + at which point the last `maxlen` accumulated messages below INFO are + printed out. This feature is only enabled under the `queue_debug` context + manager. + ''' + def __init__(self, stdout=sys.stdout, stderr=sys.stderr, maxlen=20): + super(ConfigureOutputHandler, self).__init__() + + # Python has this feature where it sets the encoding of pipes to + # ascii, which blatantly fails when trying to print out non-ascii. + def fix_encoding(fh): + try: + isatty = fh.isatty() + except AttributeError: + isatty = True + + if not isatty: + encoding = getpreferredencoding() + if encoding: + return codecs.getwriter(encoding)(fh) + return fh + + self._stdout = fix_encoding(stdout) + self._stderr = fix_encoding(stderr) if stdout != stderr else self._stdout + try: + fd1 = self._stdout.fileno() + fd2 = self._stderr.fileno() + self._same_output = self._is_same_output(fd1, fd2) + except AttributeError: + self._same_output = self._stdout == self._stderr + self._stdout_waiting = None + self._debug = deque(maxlen=maxlen + 1) + self._keep_if_debug = self.THROW + self._queue_is_active = False + + @staticmethod + def _is_same_output(fd1, fd2): + if fd1 == fd2: + return True + stat1 = os.fstat(fd1) + stat2 = os.fstat(fd2) + return stat1.st_ino == stat2.st_ino and stat1.st_dev == stat2.st_dev + + # possible values for _stdout_waiting + WAITING = 1 + INTERRUPTED = 2 + + # possible values for _keep_if_debug + THROW = 0 + KEEP = 1 + PRINT = 2 + + def emit(self, record): + try: + if record.levelno == logging.INFO: + stream = self._stdout + msg = record.getMessage() + if (self._stdout_waiting == self.INTERRUPTED and + self._same_output): + msg = ' ... %s' % msg + self._stdout_waiting = msg.endswith('... ') + if msg.endswith('... '): + self._stdout_waiting = self.WAITING + else: + self._stdout_waiting = None + msg = '%s\n' % msg + elif (record.levelno < logging.INFO and + self._keep_if_debug != self.PRINT): + if self._keep_if_debug == self.KEEP: + self._debug.append(record) + return + else: + if record.levelno >= logging.ERROR and len(self._debug): + self._emit_queue() + + if self._stdout_waiting == self.WAITING and self._same_output: + self._stdout_waiting = self.INTERRUPTED + self._stdout.write('\n') + self._stdout.flush() + stream = self._stderr + msg = '%s\n' % self.format(record) + stream.write(msg) + stream.flush() + except (KeyboardInterrupt, SystemExit): + raise + except: + self.handleError(record) + + @contextmanager + def queue_debug(self): + if self._queue_is_active: + yield + return + self._queue_is_active = True + self._keep_if_debug = self.KEEP + try: + yield + except Exception: + self._emit_queue() + # The exception will be handled and very probably printed out by + # something upper in the stack. + raise + finally: + self._queue_is_active = False + self._keep_if_debug = self.THROW + self._debug.clear() + + def _emit_queue(self): + self._keep_if_debug = self.PRINT + if len(self._debug) == self._debug.maxlen: + r = self._debug.popleft() + self.emit(logging.LogRecord( + r.name, r.levelno, r.pathname, r.lineno, + '<truncated - see config.log for full output>', + (), None)) + while True: + try: + self.emit(self._debug.popleft()) + except IndexError: + break + self._keep_if_debug = self.KEEP + + +class LineIO(object): + '''File-like class that sends each line of the written data to a callback + (without carriage returns). + ''' + def __init__(self, callback): + self._callback = callback + self._buf = '' + self._encoding = getpreferredencoding() + + def write(self, buf): + if self._encoding and isinstance(buf, str): + buf = buf.decode(self._encoding) + lines = buf.splitlines() + if not lines: + return + if self._buf: + lines[0] = self._buf + lines[0] + self._buf = '' + if not buf.endswith('\n'): + self._buf = lines.pop() + + for line in lines: + self._callback(line) + + def close(self): + if self._buf: + self._callback(self._buf) + self._buf = '' + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() |