--- a/python/mozbuild/mozbuild/configure/__init__.py
+++ b/python/mozbuild/mozbuild/configure/__init__.py
@@ -26,16 +26,17 @@ from mozbuild.configure.help import Help
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):
@@ -45,69 +46,134 @@ class ConfigureError(Exception):
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', 'sandboxed')
- def __init__(self, sandbox, func, dependencies):
+ __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 8 primitives:
+ 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.
+ 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.
@@ -160,19 +226,22 @@ class ConfigureSandbox(dict):
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/depends.
+ # 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)
@@ -348,26 +417,17 @@ class ConfigureSandbox(dict):
if self._help_option not in arg.dependencies:
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:
raise ConfigureError("Missing @depends for `%s`: '--help'" %
obj.name)
- return self._value_for_depends_real(obj)
-
- @memoize
- def _value_for_depends_real(self, obj):
- when = self._conditions.get(obj)
- if when and not self._value_for(when):
- return None
-
- resolved_args = [self._value_for(d) for d in obj.dependencies]
- return obj.func(*resolved_args)
+ 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)
@@ -436,28 +496,55 @@ class ConfigureSandbox(dict):
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 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 = kwargs.get('when')
- if when is not None:
- when = self._dependency(when, 'option', 'when')
+ 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)
@@ -496,19 +583,17 @@ class ConfigureSandbox(dict):
raise ConfigureError('@depends needs at least one argument')
for k in kwargs:
if k != 'when':
raise TypeError(
"depends_impl() got an unexpected keyword argument '%s'"
% k)
- when = kwargs.get('when')
- if when is not None:
- when = self._dependency(when, '@depends', 'when')
+ when = self._normalize_when(kwargs.get('when'), '@depends')
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)
]
@@ -517,44 +602,34 @@ class ConfigureSandbox(dict):
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)
- if when:
- self._conditions[depends] = when
-
- # Only @depends functions with a dependency on '--help' are
- # executed immediately. Everything else is queued for later
- # execution.
- if self._help_option in dependencies:
- self._value_for(depends)
- elif not self._help:
- self._execution_queue.append((self._value_for, (depends,)))
-
+ depends = DependsFunction(self, func, dependencies, when=when)
return depends.sandboxed
return decorator
- def include_impl(self, what):
+ 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.
'''
- what = self._resolve(what)
- if what:
- if not isinstance(what, types.StringTypes):
- raise TypeError("Unexpected type: '%s'" % type(what).__name__)
- self.include_file(what)
+ 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
@@ -696,32 +771,30 @@ class ConfigureSandbox(dict):
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.
'''
- if when is not None:
- when = self._dependency(when, 'set_config', 'when')
+ 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).
'''
- if when is not None:
- when = self._dependency(when, 'set_define', 'when')
+ 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
@@ -783,18 +856,17 @@ class ConfigureSandbox(dict):
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)
- if when is not None:
- when = self._dependency(when, 'imply_option', 'when')
+ 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,
--- a/python/mozbuild/mozbuild/test/configure/test_configure.py
+++ b/python/mozbuild/mozbuild/test/configure/test_configure.py
@@ -897,16 +897,88 @@ class TestConfigure(unittest.TestCase):
with self.assertRaises(TypeError) as e:
with self.moz_configure('''
include(42)
'''):
self.get_config()
self.assertEquals(e.exception.message, "Unexpected type: 'int'")
+ def test_include_when(self):
+ with MockedOpen({
+ os.path.join(test_data_path, 'moz.configure'): textwrap.dedent('''
+ @depends('--help')
+ def always(_):
+ return True
+ @depends('--help')
+ def never(_):
+ return False
+
+ option('--with-foo', help='foo')
+
+ include('always.configure', when=always)
+ include('never.configure', when=never)
+ include('foo.configure', when='--with-foo')
+
+ set_config('FOO', foo)
+ set_config('BAR', bar)
+ set_config('QUX', qux)
+ '''),
+ os.path.join(test_data_path, 'always.configure'): textwrap.dedent('''
+ option('--with-bar', help='bar')
+ @depends('--with-bar')
+ def bar(x):
+ if x:
+ return 'bar'
+ '''),
+ os.path.join(test_data_path, 'never.configure'): textwrap.dedent('''
+ option('--with-qux', help='qux')
+ @depends('--with-qux')
+ def qux(x):
+ if x:
+ return 'qux'
+ '''),
+ os.path.join(test_data_path, 'foo.configure'): textwrap.dedent('''
+ option('--with-foo-really', help='really foo')
+ @depends('--with-foo-really')
+ def foo(x):
+ if x:
+ return 'foo'
+
+ include('foo2.configure', when='--with-foo-really')
+ '''),
+ os.path.join(test_data_path, 'foo2.configure'): textwrap.dedent('''
+ set_config('FOO2', True)
+ '''),
+ }):
+ config = self.get_config()
+ self.assertEquals(config, {})
+
+ config = self.get_config(['--with-foo'])
+ self.assertEquals(config, {})
+
+ config = self.get_config(['--with-bar'])
+ self.assertEquals(config, {
+ 'BAR': 'bar',
+ })
+
+ with self.assertRaises(InvalidOptionError) as e:
+ self.get_config(['--with-qux'])
+
+ self.assertEquals(
+ e.exception.message,
+ '--with-qux is not available in this configuration'
+ )
+
+ config = self.get_config(['--with-foo', '--with-foo-really'])
+ self.assertEquals(config, {
+ 'FOO': 'foo',
+ 'FOO2': True,
+ })
+
def test_sandbox_failures(self):
with self.assertRaises(KeyError) as e:
with self.moz_configure('''
include = 42
'''):
self.get_config()
self.assertEquals(e.exception.message, 'Cannot reassign builtins')
@@ -1142,11 +1214,100 @@ class TestConfigure(unittest.TestCase):
def foo(value):
return value
'''):
self.get_config()
self.assertEquals(e.exception.message,
"Invalid argument to @imports: 'os*'")
+ def test_only_when(self):
+ moz_configure = '''
+ option('--enable-when', help='when')
+ @depends('--enable-when', '--help')
+ def when(value, _):
+ return bool(value)
+
+ with only_when(when):
+ option('--foo', nargs='*', help='foo')
+ @depends('--foo')
+ def foo(value):
+ return value
+
+ set_config('FOO', foo)
+ set_define('FOO', foo)
+
+ # It is possible to depend on a function defined in a only_when
+ # block. It then resolves to `None`.
+ set_config('BAR', depends(foo)(lambda x: x))
+ set_define('BAR', depends(foo)(lambda x: x))
+ '''
+
+ with self.moz_configure(moz_configure):
+ config = self.get_config()
+ self.assertEqual(config, {
+ 'DEFINES': {},
+ })
+
+ config = self.get_config(['--enable-when'])
+ self.assertEqual(config, {
+ 'BAR': NegativeOptionValue(),
+ 'FOO': NegativeOptionValue(),
+ 'DEFINES': {
+ 'BAR': NegativeOptionValue(),
+ 'FOO': NegativeOptionValue(),
+ },
+ })
+
+ config = self.get_config(['--enable-when', '--foo=bar'])
+ self.assertEqual(config, {
+ 'BAR': PositiveOptionValue(['bar']),
+ 'FOO': PositiveOptionValue(['bar']),
+ 'DEFINES': {
+ 'BAR': PositiveOptionValue(['bar']),
+ 'FOO': PositiveOptionValue(['bar']),
+ },
+ })
+
+ # The --foo option doesn't exist when --enable-when is not given.
+ with self.assertRaises(InvalidOptionError) as e:
+ self.get_config(['--foo'])
+
+ self.assertEquals(e.exception.message,
+ '--foo is not available in this configuration')
+
+ # Cannot depend on an option defined in a only_when block, because we
+ # don't know what OptionValue would make sense.
+ with self.moz_configure(moz_configure + '''
+ set_config('QUX', depends('--foo')(lambda x: x))
+ '''):
+ with self.assertRaises(ConfigureError) as e:
+ self.get_config()
+
+ self.assertEquals(e.exception.message,
+ '@depends function needs the same `when` as '
+ 'options it depends on')
+
+ with self.moz_configure(moz_configure + '''
+ set_config('QUX', depends('--foo', when=when)(lambda x: x))
+ '''):
+ self.get_config(['--enable-when'])
+
+ # Using imply_option for an option defined in a only_when block fails
+ # similarly if the imply_option happens outside the block.
+ with self.moz_configure('''
+ imply_option('--foo', True)
+ ''' + moz_configure):
+ with self.assertRaises(InvalidOptionError) as e:
+ self.get_config()
+
+ self.assertEquals(e.exception.message,
+ '--foo is not available in this configuration')
+
+ # And similarly doesn't fail when the condition is true.
+ with self.moz_configure('''
+ imply_option('--foo', True)
+ ''' + moz_configure):
+ self.get_config(['--enable-when'])
+
if __name__ == '__main__':
main()