summaryrefslogtreecommitdiffstats
path: root/python/mozbuild/mozbuild/configure
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /python/mozbuild/mozbuild/configure
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-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__.py935
-rw-r--r--python/mozbuild/mozbuild/configure/check_debug_ranges.py62
-rw-r--r--python/mozbuild/mozbuild/configure/constants.py103
-rw-r--r--python/mozbuild/mozbuild/configure/help.py45
-rw-r--r--python/mozbuild/mozbuild/configure/libstdcxx.py81
-rw-r--r--python/mozbuild/mozbuild/configure/lint.py78
-rw-r--r--python/mozbuild/mozbuild/configure/lint_util.py52
-rw-r--r--python/mozbuild/mozbuild/configure/options.py485
-rw-r--r--python/mozbuild/mozbuild/configure/util.py226
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()