--- a/python/moz.build
+++ b/python/moz.build
@@ -37,16 +37,17 @@ PYTHON_UNIT_TESTS += [
'mozbuild/mozbuild/test/backend/test_android_eclipse.py',
'mozbuild/mozbuild/test/backend/test_build.py',
'mozbuild/mozbuild/test/backend/test_configenvironment.py',
'mozbuild/mozbuild/test/backend/test_recursivemake.py',
'mozbuild/mozbuild/test/backend/test_visualstudio.py',
'mozbuild/mozbuild/test/common.py',
'mozbuild/mozbuild/test/compilation/__init__.py',
'mozbuild/mozbuild/test/compilation/test_warnings.py',
+ 'mozbuild/mozbuild/test/configure/test_configure.py',
'mozbuild/mozbuild/test/configure/test_options.py',
'mozbuild/mozbuild/test/controller/__init__.py',
'mozbuild/mozbuild/test/controller/test_ccachestats.py',
'mozbuild/mozbuild/test/controller/test_clobber.py',
'mozbuild/mozbuild/test/frontend/__init__.py',
'mozbuild/mozbuild/test/frontend/test_context.py',
'mozbuild/mozbuild/test/frontend/test_emitter.py',
'mozbuild/mozbuild/test/frontend/test_namespaces.py',
--- a/python/mozbuild/mozbuild/configure/__init__.py
+++ b/python/mozbuild/mozbuild/configure/__init__.py
@@ -0,0 +1,428 @@
+# 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 os
+import sys
+import types
+from collections import OrderedDict
+from functools import wraps
+from mozbuild.configure.options import (
+ CommandLineHelper,
+ ConflictingOption,
+ InvalidOption,
+ Option,
+ OptionValue,
+)
+from mozbuild.configure.help import HelpFormatter
+from mozbuild.util import (
+ ReadOnlyDict,
+ ReadOnlyNamespace,
+)
+import mozpack.path as mozpath
+
+
+class ConfigureError(Exception):
+ pass
+
+
+class DummyFunction(object):
+ '''Sandbox-visible representation of @depends functions.'''
+ def __call__(self, *arg, **kwargs):
+ raise RuntimeError('The `%s` function may not be called'
+ % self.__name__)
+
+
+class SandboxedGlobal(dict):
+ '''Identifiable dict type for use as function global'''
+
+
+class DependsOutput(dict):
+ '''Dict holding the results yielded by a @depends function.'''
+ __slots__ = ('implied_options',)
+
+ def __init__(self):
+ super(DependsOutput, self).__init__()
+ self.implied_options = []
+
+ def imply_option(self, option, reason=None):
+ if not isinstance(option, types.StringTypes):
+ raise TypeError('imply_option must be given a string')
+ self.implied_options.append((option, reason))
+
+
+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 5 primitives:
+ - option
+ - depends
+ - template
+ - advanced
+ - include
+
+ `option` and `include` are functions. `depends`, `template` and `advanced`
+ are decorators.
+
+ Additional primitives should be frowned upon to keep the sandbox itself as
+ simple as possible. Instead, helpers shall 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.
+ BUILTINS = ReadOnlyDict({
+ b: __builtins__[b]
+ for b in ('None', 'False', 'True', 'int', 'bool', 'any', 'all', 'len',
+ 'list', 'set', 'dict')
+ }, __import__=forbidden_import)
+
+ # Expose a limited set of functions from os.path
+ OS = ReadOnlyNamespace(path=ReadOnlyNamespace(
+ abspath=os.path.abspath,
+ basename=os.path.basename,
+ dirname=os.path.dirname,
+ exists=os.path.exists,
+ isabs=os.path.isabs,
+ isdir=os.path.isdir,
+ isfile=os.path.isfile,
+ join=os.path.join,
+ normpath=os.path.normpath,
+ realpath=os.path.realpath,
+ relpath=os.path.relpath,
+ ))
+
+ def __init__(self, config, environ=os.environ, argv=sys.argv,
+ stdout=sys.stdout, stderr=sys.stderr):
+ dict.__setitem__(self, '__builtins__', self.BUILTINS)
+
+ self._paths = []
+ self._templates = set()
+ self._depends = {}
+ self._seen = set()
+
+ self._options = OrderedDict()
+ # Store the raw values returned by @depends functions
+ self._results = {}
+ # Store several kind of information:
+ # - value for each Option, as per returned by Option.get_value
+ # - raw option (as per command line or environment) for each value
+ # - config set by each @depends function
+ self._db = {}
+
+ self._implied_options = {}
+
+ self._helper = CommandLineHelper(environ, argv)
+
+ self._config, self._stdout, self._stderr = config, stdout, stderr
+
+ self._help = None
+ self._help_option = self.option_impl('--help',
+ help='print this message')
+ self._seen.add(self._help_option)
+ if self._db[self._help_option]:
+ self._help = HelpFormatter(argv[0])
+ self._help.add(self._help_option)
+
+ def exec_file(self, path):
+ '''Execute one file within the sandbox. Users of this class probably
+ want to use `run` instead.'''
+
+ if self._paths:
+ path = mozpath.join(mozpath.dirname(self._paths[-1]), 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.abspath(path)
+ if path in self._paths:
+ raise ConfigureError(
+ 'Cannot include `%s` because it was included already.' % path)
+ self._paths.append(path)
+
+ source = open(path, 'rb').read()
+
+ code = compile(source, path, 'exec')
+
+ exec(code, self)
+
+ self._paths.pop(-1)
+
+ def run(self, path):
+ '''Executes the given file within the sandbox, and ensure the overall
+ consistency of the executed script.'''
+ self.exec_file(path)
+
+ # All command line arguments should have been removed (handled) by now.
+ for arg in self._helper:
+ without_value = arg.split('=', 1)[0]
+ if arg in self._implied_options:
+ func, reason = self._implied_options[arg]
+ raise ConfigureError(
+ '`%s`, emitted by `%s` in `%s`, was not handled.'
+ % (without_value, func.__name__,
+ func.func_code.co_filename))
+ raise InvalidOption('Unknown option: %s' % without_value)
+
+ # All options must be referenced by some @depends function
+ for option in self._options.itervalues():
+ if option not in self._seen:
+ raise ConfigureError(
+ 'Option `%s` is not referenced by any @depends'
+ % option.option
+ )
+
+ if self._help:
+ self._help.usage(self._stdout)
+
+ 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 (not isinstance(value, DummyFunction) and
+ value not in self._templates):
+ 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):
+ if isinstance(arg, DummyFunction):
+ assert arg in self._depends
+ func = self._depends[arg]
+ assert not inspect.isgeneratorfunction(func)
+ assert func in self._results
+ if not func.with_help:
+ raise ConfigureError("Missing @depends for `%s`: '--help'" %
+ func.__name__)
+ self._seen.add(func)
+ result = self._results[func]
+ return result
+ return arg
+
+ def option_impl(self, *args, **kwargs):
+ '''Implementation of option()
+ This function creates and returns an Option() object, passing it the
+ resolved arguments (use 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.
+ '''
+ args = [self._resolve(arg) for arg in args]
+ kwargs = {k: self._resolve(v) for k, v in kwargs.iteritems()}
+ option = Option(*args, **kwargs)
+ if option.name in self._options:
+ raise ConfigureError('Option `%s` already defined'
+ % self._options[option.name].option)
+ if option.env in self._options:
+ raise ConfigureError('Option `%s` already defined'
+ % self._options[option.env].option)
+ if option.name:
+ self._options[option.name] = option
+ if option.env:
+ self._options[option.env] = option
+
+ try:
+ value, option_string = self._helper.handle(option)
+ except ConflictingOption as e:
+ func, reason = self._implied_options[e.arg]
+ raise InvalidOption(
+ "'%s' implied by '%s' conflicts with '%s' from the %s"
+ % (e.arg, reason, e.old_arg, e.old_origin))
+
+ if self._help:
+ self._help.add(option)
+
+ self._db[option] = value
+ self._db[value] = (option_string.split('=', 1)[0]
+ if option_string else option_string)
+ return option
+
+ def depends_impl(self, *args):
+ '''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, and two additional functions:
+ `imply_option` and `set_config`. The former allows to inject additional
+ options as if they had been passed on the command line. The latter
+ declares new configuration items.
+ '''
+ if not args:
+ raise ConfigureError('@depends needs at least one argument')
+
+ with_help = False
+ resolved_args = []
+ for arg in args:
+ 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]
+ if arg == self._help_option:
+ with_help = True
+ elif isinstance(arg, DummyFunction):
+ assert arg in self._depends
+ arg = self._depends[arg]
+ else:
+ raise TypeError(
+ "Cannot use object of type '%s' as argument to @depends"
+ % type(arg))
+ self._seen.add(arg)
+ resolved_arg = self._results.get(arg)
+ if resolved_arg is None:
+ assert arg in self._db or self._help
+ resolved_arg = self._db.get(arg)
+ resolved_args.append(resolved_arg)
+
+ def decorator(func):
+ if inspect.isgeneratorfunction(func):
+ raise ConfigureError(
+ 'Cannot decorate generator functions with @depends')
+ func, glob = self._prepare_function(func)
+ result = DependsOutput()
+ glob.update(
+ imply_option=result.imply_option,
+ set_config=result.__setitem__,
+ )
+ dummy = wraps(func)(DummyFunction())
+ self._depends[dummy] = func
+ func.with_help = with_help
+ if self._help and not with_help:
+ return dummy
+
+ self._results[func] = func(*resolved_args)
+ self._db[func] = ReadOnlyDict(result)
+
+ for option, reason in result.implied_options:
+ self._helper.add(option, 'implied')
+ if not reason:
+ deps = []
+ for name, value in zip(args, resolved_args):
+ if not isinstance(value, OptionValue):
+ raise ConfigureError(
+ "Cannot infer what implied '%s'" % option)
+ if name == '--help':
+ continue
+ deps.append(value.format(self._db.get(value) or name))
+ if len(deps) != 1:
+ raise ConfigureError(
+ "Cannot infer what implied '%s'" % option)
+ reason = deps[0]
+
+ self._implied_options[option] = func, reason
+
+ if not self._help:
+ for k, v in result.iteritems():
+ if k in self._config:
+ raise ConfigureError(
+ "Cannot add '%s' to configuration: Key already "
+ "exists" % k)
+ self._config[k] = v
+
+ return dummy
+
+ return decorator
+
+ def include_impl(self, what):
+ '''Implementation of include().
+ Allows to include external files for execution in the sandbox.
+ '''
+ what = self._resolve(what)
+ if what:
+ if not isinstance(what, types.StringTypes):
+ raise TypeError("Unexpected type: '%s'" % type(what))
+ self.exec_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 `advanced`,
+ `depends` and `option`.
+ Templates allow to simplify repetitive constructs, or to implement
+ helper decorators and somesuch.
+ '''
+ template, glob = self._prepare_function(func)
+ glob.update(
+ advanced=self.advanced_impl,
+ depends=self.depends_impl,
+ option=self.option_impl,
+ )
+ self._templates.add(template)
+ return template
+
+ def advanced_impl(self, func):
+ '''Implementation of @advanced.
+ This function gives the decorated function access to the complete set
+ of builtins, allowing the import keyword as an expected side effect.
+ '''
+ func, glob = self._prepare_function(func)
+ glob.update(__builtins__=__builtins__)
+ return func
+
+ def _prepare_function(self, func):
+ '''Alter the given function global namespace with the common ground
+ for @depends, @template and @advanced.
+ '''
+ if not inspect.isfunction(func):
+ raise TypeError("Unexpected type: '%s'" % type(func))
+ if isinstance(func.func_globals, SandboxedGlobal):
+ return func, func.func_globals
+
+ glob = SandboxedGlobal(func.func_globals)
+ glob.update(
+ __builtins__=self.BUILTINS,
+ __file__=self._paths[-1],
+ os=self.OS,
+ )
+ func = wraps(func)(types.FunctionType(
+ func.func_code,
+ glob,
+ func.__name__,
+ func.func_defaults,
+ func.func_closure
+ ))
+ return func, glob
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/configure/help.py
@@ -0,0 +1,40 @@
+# 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)
+ # 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)
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/configure/data/extra.configure
@@ -0,0 +1,11 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=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/.
+
+option('--extra', help='Extra')
+
+@depends('--extra')
+def extra(extra):
+ set_config('EXTRA', extra)
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/configure/data/included.configure
@@ -0,0 +1,47 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=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/.
+
+# For more complex and repetitive things, we can create templates
+@template
+def check_compiler_flag(flag):
+ @depends(is_gcc)
+ def check(value):
+ if value:
+ set_config('CFLAGS', [flag])
+
+ return check
+
+check_compiler_flag('-Werror=foobar')
+
+# A template that doesn't return functions can be used in @depends functions.
+@template
+def fortytwo():
+ return 42
+
+@template
+def twentyone():
+ yield 21
+
+@depends(is_gcc)
+def check(value):
+ if value:
+ set_config('TEMPLATE_VALUE', fortytwo())
+ for val in twentyone():
+ set_config('TEMPLATE_VALUE_2', val)
+
+# Templates can use @advanced too to import modules and get the full set of
+# builtins.
+@template
+@advanced
+def platform():
+ import sys
+ return sys.platform
+
+option('--enable-advanced-template', help='Advanced template')
+@depends('--enable-advanced-template')
+def check(value):
+ if value:
+ set_config('PLATFORM', platform())
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/configure/data/moz.configure
@@ -0,0 +1,195 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=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/.
+
+option('--enable-simple', help='Enable simple')
+
+# Setting MOZ_WITH_ENV in the environment has the same effect as passing
+# --enable-with-env.
+option('--enable-with-env', env='MOZ_WITH_ENV', help='Enable with env')
+
+# Optional values
+option('--enable-values', nargs='*', help='Enable values')
+
+# Everything supported in the Option class is supported in option(). Assume
+# the tests of the Option class are extensive about this.
+
+# Alternatively to --enable/--disable, there also is --with/--without. The
+# difference is semantic only. Behavior is the same as --enable/--disable.
+
+# When the option name starts with --disable/--without, the default is for
+# the option to be enabled.
+option('--without-thing', help='Build without thing')
+
+# A --enable/--with option with a default of False is equivalent to a
+# --disable/--without option. This can be used to change the defaults
+# depending on e.g. the target or the built application.
+option('--with-stuff', default=False, help='Build with stuff')
+
+# Other kinds of arbitrary options are also allowed. This is effectively
+# equivalent to --enable/--with, with no possibility of --disable/--without.
+option('--option', env='MOZ_OPTION', help='Option')
+
+# It is also possible to pass options through the environment only.
+option(env='CC', nargs=1, help='C Compiler')
+
+# Call the function when the --enable-simple option is processed, with its
+# OptionValue as argument.
+@depends('--enable-simple')
+def simple(simple):
+ if simple:
+ set_config('ENABLED_SIMPLE', simple)
+
+# There can be multiple functions depending on the same option.
+@depends('--enable-simple')
+def simple(simple):
+ set_config('SIMPLE', simple)
+
+@depends('--enable-with-env')
+def with_env(with_env):
+ set_config('WITH_ENV', with_env)
+
+# It doesn't matter if the dependency is on --enable or --disable
+@depends('--disable-values')
+def with_env2(values):
+ set_config('VALUES', values)
+
+# It is possible to @depends on environment-only options.
+@depends('CC')
+def is_gcc(cc):
+ return cc and 'gcc' in cc[0]
+
+@depends(is_gcc)
+def is_gcc_check(is_gcc):
+ set_config('IS_GCC', is_gcc)
+
+# It is possible to depend on the result from another function.
+# The input argument is a dict fed with the elements the function implied.
+@depends(with_env2)
+def with_env3(values):
+ set_config('VALUES2', values['VALUES'])
+
+# @depends functions can also return results for use as input to another
+# @depends.
+@depends(with_env3)
+def with_env4(values):
+ return values['VALUES2']
+
+@depends(with_env4)
+def with_env5(values):
+ set_config('VALUES3', values)
+
+# The result from @depends functions can also be used as input to options.
+# The result must be returned, not implied. The function must also depend
+# on --help.
+@depends('--enable-simple', '--help')
+def simple(simple, help):
+ return 'simple' if simple else 'not-simple'
+
+option('--with-returned-default', default=simple, help='Returned default')
+
+@depends('--with-returned-default')
+def default(value):
+ set_config('DEFAULTED', value)
+
+# @depends functions can also declare that some extra options are implied.
+# Those options need to be defined _after_ the function, and they mustn't
+# appear on the command line or the environment with conflicting values.
+@depends('--enable-values')
+def values(values):
+ if values:
+ imply_option('--enable-implied')
+ imply_option(values.format('--with-implied-values'))
+ imply_option(values.format('WITH_IMPLIED_ENV'))
+
+option('--enable-implied', help='Implied')
+
+option('--with-implied-values', nargs='*', help='Implied values')
+
+option(env='WITH_IMPLIED_ENV', nargs='*', help='Implied env')
+
+@depends('--enable-implied')
+def implied(value):
+ set_config('IMPLIED', value)
+
+@depends('--with-implied-values')
+def implied(values):
+ set_config('IMPLIED_VALUES', values)
+
+@depends('WITH_IMPLIED_ENV')
+def implied(values):
+ set_config('IMPLIED_ENV', values)
+
+@depends('--enable-values', '--help')
+def choices(values, help):
+ if len(values):
+ return {
+ 'alpha': ('a', 'b', 'c'),
+ 'numeric': ('0', '1', '2'),
+ }.get(values[0])
+
+option('--returned-choices', choices=choices, help='Choices')
+
+@depends('--returned-choices')
+def returned_choices(values):
+ set_config('CHOICES', values)
+
+# All options must be referenced by some @depends function.
+# It is possible to depend on multiple options/functions
+@depends('--without-thing', '--with-stuff', with_env4, '--option')
+def remainder(*args):
+ set_config('REMAINDER', args)
+
+# It is possible to include other files to extend the configuration script.
+include('included.configure')
+
+# It is also possible for the include file path to come from the result of a
+# @depends function. That function needs to depend on '--help' like for option
+# defaults and choices.
+option('--enable-include', nargs=1, help='Include')
+@depends('--enable-include', '--help')
+def include_path(path, help):
+ return path[0] if path else None
+
+include(include_path)
+
+# @advanced functions are allowed to import modules and have access to
+# the standard builtins instead of restricted ones. The order of the decorators
+# matter: @advanced needs to appear last.
+option('--with-advanced', nargs='?', help='Advanced')
+@depends('--with-advanced')
+@advanced
+def with_advanced(value):
+ if value:
+ from mozbuild.configure.options import OptionValue
+ set_config('ADVANCED', isinstance(value, OptionValue))
+
+# Trying to import without @advanced will fail at runtime.
+@depends('--with-advanced')
+def with_advanced(value):
+ if len(value) and value[0] == 'break':
+ from mozbuild.configure.options import OptionValue
+ set_config('ADVANCED', isinstance(value, OptionValue))
+
+# A limited set of functions from os.path are exposed to non @advanced
+# functions.
+@depends('--with-advanced')
+def with_advanced(value):
+ if len(value):
+ set_config('IS_FILE', os.path.isfile(value[0]))
+
+# An @advanced function can still import the full set.
+@depends('--with-advanced')
+@advanced
+def with_advanced(value):
+ if len(value):
+ import os.path
+ set_config('HAS_GETATIME', hasattr(os.path, 'getatime'))
+
+@depends('--with-advanced')
+@advanced
+def with_advanced(value):
+ if len(value):
+ set_config('HAS_GETATIME2', hasattr(os.path, 'getatime'))
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/configure/test_configure.py
@@ -0,0 +1,307 @@
+# 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
+import sys
+import unittest
+
+from mozunit import main
+
+from mozbuild.configure.options import (
+ InvalidOption,
+ NegativeOptionValue,
+ PositiveOptionValue,
+)
+from mozbuild.configure import ConfigureSandbox
+
+import mozpack.path as mozpath
+
+test_data_path = mozpath.abspath(mozpath.dirname(__file__))
+test_data_path = mozpath.join(test_data_path, 'data')
+
+
+class TestConfigure(unittest.TestCase):
+ def get_result(self, args=[], environ={}, prog='/bin/configure'):
+ config = {}
+ out = StringIO()
+ sandbox = ConfigureSandbox(config, environ, [prog] + args, out, out)
+
+ sandbox.run(mozpath.join(test_data_path, 'moz.configure'))
+
+ return config, out.getvalue()
+
+ def get_config(self, options=[], env={}):
+ config, out = self.get_result(options, environ=env)
+ self.assertEquals('', out)
+ return config
+
+ def test_defaults(self):
+ config = self.get_config()
+ self.maxDiff = None
+ self.assertEquals({
+ 'CHOICES': NegativeOptionValue(),
+ 'DEFAULTED': PositiveOptionValue(('not-simple',)),
+ 'IS_GCC': NegativeOptionValue(),
+ 'REMAINDER': (PositiveOptionValue(), NegativeOptionValue(),
+ NegativeOptionValue(), NegativeOptionValue()),
+ 'SIMPLE': NegativeOptionValue(),
+ 'VALUES': NegativeOptionValue(),
+ 'VALUES2': NegativeOptionValue(),
+ 'VALUES3': NegativeOptionValue(),
+ 'WITH_ENV': NegativeOptionValue(),
+ 'IMPLIED': NegativeOptionValue(),
+ 'IMPLIED_ENV': NegativeOptionValue(),
+ 'IMPLIED_VALUES': NegativeOptionValue(),
+ }, config)
+
+ def test_help(self):
+ config, out = self.get_result(['--help'])
+ self.assertEquals({}, config)
+ self.maxDiff = None
+ self.assertEquals(
+ 'Usage: configure [options]\n'
+ '\n'
+ 'Options: [defaults in brackets after descriptions]\n'
+ ' --help print this message\n'
+ ' --enable-simple Enable simple\n'
+ ' --enable-with-env Enable with env\n'
+ ' --enable-values Enable values\n'
+ ' --without-thing Build without thing\n'
+ ' --with-stuff Build with stuff\n'
+ ' --option Option\n'
+ ' --with-returned-default Returned default [not-simple]\n'
+ ' --enable-implied Implied\n'
+ ' --with-implied-values Implied values\n'
+ ' --returned-choices Choices\n'
+ ' --enable-advanced-template\n'
+ ' Advanced template\n'
+ ' --enable-include Include\n'
+ ' --with-advanced Advanced\n'
+ '\n'
+ 'Environment variables:\n'
+ ' CC C Compiler\n'
+ ' WITH_IMPLIED_ENV Implied env\n',
+ out
+ )
+
+ def test_unknown(self):
+ with self.assertRaises(InvalidOption):
+ self.get_config(['--unknown'])
+
+ def test_simple(self):
+ for config in (
+ self.get_config(),
+ self.get_config(['--disable-simple']),
+ # Last option wins.
+ self.get_config(['--enable-simple', '--disable-simple']),
+ ):
+ self.assertNotIn('ENABLED_SIMPLE', config)
+ self.assertIn('SIMPLE', config)
+ self.assertEquals(NegativeOptionValue(), config['SIMPLE'])
+
+ for config in (
+ self.get_config(['--enable-simple']),
+ self.get_config(['--disable-simple', '--enable-simple']),
+ ):
+ self.assertIn('ENABLED_SIMPLE', config)
+ self.assertIn('SIMPLE', config)
+ self.assertEquals(PositiveOptionValue(), config['SIMPLE'])
+ self.assertIs(config['SIMPLE'], config['ENABLED_SIMPLE'])
+
+ # --enable-simple doesn't take values.
+ with self.assertRaises(InvalidOption):
+ self.get_config(['--enable-simple=value'])
+
+ def test_with_env(self):
+ for config in (
+ self.get_config(),
+ self.get_config(['--disable-with-env']),
+ self.get_config(['--enable-with-env', '--disable-with-env']),
+ self.get_config(env={'MOZ_WITH_ENV': ''}),
+ # Options win over environment
+ self.get_config(['--disable-with-env'],
+ env={'MOZ_WITH_ENV': '1'}),
+ ):
+ self.assertIn('WITH_ENV', config)
+ self.assertEquals(NegativeOptionValue(), config['WITH_ENV'])
+
+ for config in (
+ self.get_config(['--enable-with-env']),
+ self.get_config(['--disable-with-env', '--enable-with-env']),
+ self.get_config(env={'MOZ_WITH_ENV': '1'}),
+ self.get_config(['--enable-with-env'],
+ env={'MOZ_WITH_ENV': ''}),
+ ):
+ self.assertIn('WITH_ENV', config)
+ self.assertEquals(PositiveOptionValue(), config['WITH_ENV'])
+
+ with self.assertRaises(InvalidOption):
+ self.get_config(['--enable-with-env=value'])
+
+ with self.assertRaises(InvalidOption):
+ self.get_config(env={'MOZ_WITH_ENV': 'value'})
+
+ def test_values(self, name='VALUES'):
+ for config in (
+ self.get_config(),
+ self.get_config(['--disable-values']),
+ self.get_config(['--enable-values', '--disable-values']),
+ ):
+ self.assertIn(name, config)
+ self.assertEquals(NegativeOptionValue(), config[name])
+
+ for config in (
+ self.get_config(['--enable-values']),
+ self.get_config(['--disable-values', '--enable-values']),
+ ):
+ self.assertIn(name, config)
+ self.assertEquals(PositiveOptionValue(), config[name])
+
+ config = self.get_config(['--enable-values=foo'])
+ self.assertIn(name, config)
+ self.assertEquals(PositiveOptionValue(('foo',)), config[name])
+
+ config = self.get_config(['--enable-values=foo,bar'])
+ self.assertIn(name, config)
+ self.assertTrue(config[name])
+ self.assertEquals(PositiveOptionValue(('foo', 'bar')), config[name])
+
+ def test_values2(self):
+ self.test_values('VALUES2')
+
+ def test_values3(self):
+ self.test_values('VALUES3')
+
+ def test_returned_default(self):
+ config = self.get_config(['--enable-simple'])
+ self.assertIn('DEFAULTED', config)
+ self.assertEquals(
+ PositiveOptionValue(('simple',)), config['DEFAULTED'])
+
+ config = self.get_config(['--disable-simple'])
+ self.assertIn('DEFAULTED', config)
+ self.assertEquals(
+ PositiveOptionValue(('not-simple',)), config['DEFAULTED'])
+
+ def test_implied_options(self):
+ config = self.get_config(['--enable-values'])
+ self.assertIn('IMPLIED', config)
+ self.assertIn('IMPLIED_VALUES', config)
+ self.assertIn('IMPLIED_ENV', config)
+ self.assertEquals(PositiveOptionValue(), config['IMPLIED'])
+ self.assertEquals(PositiveOptionValue(), config['IMPLIED_VALUES'])
+ self.assertEquals(PositiveOptionValue(), config['IMPLIED_ENV'])
+
+ config = self.get_config(['--enable-values=a'])
+ self.assertIn('IMPLIED', config)
+ self.assertIn('IMPLIED_VALUES', config)
+ self.assertIn('IMPLIED_ENV', config)
+ self.assertEquals(PositiveOptionValue(), config['IMPLIED'])
+ self.assertEquals(
+ PositiveOptionValue(('a',)), config['IMPLIED_VALUES'])
+ self.assertEquals(PositiveOptionValue(('a',)), config['IMPLIED_ENV'])
+
+ config = self.get_config(['--enable-values=a,b'])
+ self.assertIn('IMPLIED', config)
+ self.assertIn('IMPLIED_VALUES', config)
+ self.assertIn('IMPLIED_ENV', config)
+ self.assertEquals(PositiveOptionValue(), config['IMPLIED'])
+ self.assertEquals(
+ PositiveOptionValue(('a', 'b')), config['IMPLIED_VALUES'])
+ self.assertEquals(
+ PositiveOptionValue(('a', 'b')), config['IMPLIED_ENV'])
+
+ config = self.get_config(['--disable-values'])
+ self.assertIn('IMPLIED', config)
+ self.assertIn('IMPLIED_VALUES', config)
+ self.assertIn('IMPLIED_ENV', config)
+ self.assertEquals(NegativeOptionValue(), config['IMPLIED'])
+ self.assertEquals(NegativeOptionValue(), config['IMPLIED_VALUES'])
+ self.assertEquals(NegativeOptionValue(), config['IMPLIED_ENV'])
+
+ # --enable-values implies --enable-implied, which conflicts with
+ # --disable-implied
+ with self.assertRaises(InvalidOption):
+ self.get_config(['--enable-values', '--disable-implied'])
+
+ def test_returned_choices(self):
+ for val in ('a', 'b', 'c'):
+ config = self.get_config(
+ ['--enable-values=alpha', '--returned-choices=%s' % val])
+ self.assertIn('CHOICES', config)
+ self.assertEquals(PositiveOptionValue((val,)), config['CHOICES'])
+
+ for val in ('0', '1', '2'):
+ config = self.get_config(
+ ['--enable-values=numeric', '--returned-choices=%s' % val])
+ self.assertIn('CHOICES', config)
+ self.assertEquals(PositiveOptionValue((val,)), config['CHOICES'])
+
+ with self.assertRaises(InvalidOption):
+ self.get_config(['--enable-values=numeric',
+ '--returned-choices=a'])
+
+ with self.assertRaises(InvalidOption):
+ self.get_config(['--enable-values=alpha', '--returned-choices=0'])
+
+ def test_included(self):
+ config = self.get_config(env={'CC': 'gcc'})
+ self.assertIn('IS_GCC', config)
+ self.assertEquals(config['IS_GCC'], True)
+
+ config = self.get_config(
+ ['--enable-include=extra.configure', '--extra'])
+ self.assertIn('EXTRA', config)
+ self.assertEquals(PositiveOptionValue(), config['EXTRA'])
+
+ with self.assertRaises(InvalidOption):
+ self.get_config(['--extra'])
+
+ def test_template(self):
+ config = self.get_config(env={'CC': 'gcc'})
+ self.assertIn('CFLAGS', config)
+ self.assertEquals(config['CFLAGS'], ['-Werror=foobar'])
+
+ config = self.get_config(env={'CC': 'clang'})
+ self.assertNotIn('CFLAGS', config)
+
+ def test_advanced(self):
+ config = self.get_config(['--with-advanced'])
+ self.assertIn('ADVANCED', config)
+ self.assertEquals(config['ADVANCED'], True)
+
+ with self.assertRaises(ImportError):
+ self.get_config(['--with-advanced=break'])
+
+ def test_os_path(self):
+ config = self.get_config(['--with-advanced=%s' % __file__])
+ self.assertIn('IS_FILE', config)
+ self.assertEquals(config['IS_FILE'], True)
+
+ config = self.get_config(['--with-advanced=%s.no-exist' % __file__])
+ self.assertIn('IS_FILE', config)
+ self.assertEquals(config['IS_FILE'], False)
+
+ self.assertIn('HAS_GETATIME', config)
+ self.assertEquals(config['HAS_GETATIME'], True)
+ self.assertIn('HAS_GETATIME2', config)
+ self.assertEquals(config['HAS_GETATIME2'], False)
+
+ def test_template_call(self):
+ config = self.get_config(env={'CC': 'gcc'})
+ self.assertIn('TEMPLATE_VALUE', config)
+ self.assertEquals(config['TEMPLATE_VALUE'], 42)
+ self.assertIn('TEMPLATE_VALUE_2', config)
+ self.assertEquals(config['TEMPLATE_VALUE_2'], 21)
+
+ def test_template_advanced(self):
+ config = self.get_config(['--enable-advanced-template'])
+ self.assertIn('PLATFORM', config)
+ self.assertEquals(config['PLATFORM'], sys.platform)
+
+
+if __name__ == '__main__':
+ main()