Skip to content

Commit

Permalink
Add UserFeatureOption type
Browse files Browse the repository at this point in the history
This is a special type of option to be passed to most 'required' keyword
arguments. It adds a 3rd state to the traditional boolean value to cause
those methods to always return not-found even if the dependency could be
found.

Since integrators doesn't want enabled features to be a surprise there
is a global option "feature_options" to enable or disable all
automatic features.
  • Loading branch information
xclaesse committed May 31, 2018
1 parent a87496a commit ad3faab
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 29 deletions.
31 changes: 31 additions & 0 deletions docs/markdown/Build-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ option('combo_opt', type : 'combo', choices : ['one', 'two', 'three'], value : '
option('integer_opt', type : 'integer', min : 0, max : 5, value : 3) # Since 0.45.0
option('free_array_opt', type : 'array', value : ['one', 'two'])
option('array_opt', type : 'array', choices : ['one', 'two', 'three'], value : ['one', 'two'])
option('some_feature', type : 'feature', value : 'enabled')
```

All types allow a `description` value to be set describing the option,
Expand Down Expand Up @@ -60,6 +61,36 @@ default.

This type is available since version 0.44.0

### Features

A `feature` option has three states: `true`, `false` or `auto`. It is intended
to be passed as value for the `required` keyword argument of most functions.
Currently supported in
[`dependency()`](Reference-manual.md#dependency),
[`find_library()`](Reference-manual.md#compiler-object),
[`find_program()`](Reference-manual.md#find_program) and
[`add_languages()`](Reference-manual.md#add_languages) functions.

- `enabled` is the same as passing `required: true`.
- `auto` is the same as passing `required: false`.
- `disabled` do not look for the dependency and return 'not-found'.

```meson
d = dependency('foo', required : get_option('myfeature'))
if d.found()
app = executable('myapp', 'main.c', dependencies : [d])
endif
```

If the value of a `feature` option is set to `auto`, that value is overriden by
the global `feature_options` option (which defaults to `auto`). This is intended
to be used by packagers who want to have full control on which dependencies are
required and which are disabled, and not rely on build-deps being installed
(at the right version) to get a feature enabled. They could set
`feature_options=enabled` to enable all features and
disabled explicitly only the few they don't want, if any.

This type is available since version 0.47.0

## Using build options

Expand Down
12 changes: 9 additions & 3 deletions docs/markdown/Reference-manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ endif
Takes one keyword argument, `required`. It defaults to `true`, which
means that if any of the languages specified is not found, Meson will
halt. Returns true if all languages specified were found and false
otherwise.
otherwise. Since *0.47.0* the value of a [`feature`](Build-options.md#features)
option can also be passed to the `required` keyword argument.

### add_project_arguments()

Expand Down Expand Up @@ -338,7 +339,8 @@ otherwise. This function supports the following keyword arguments:
cross compiled binary will run on), usually only needed if you build
a tool to be used during compilation.
- `required`, when set to false, Meson will proceed with the build
even if the dependency is not found
even if the dependency is not found. Since *0.47.0* the value of a
[`feature`](Build-options.md#features) option can also be passed.
- `static` tells the dependency provider to try to get static
libraries instead of dynamic ones (note that this is not supported
by all dependency backends)
Expand Down Expand Up @@ -518,7 +520,9 @@ Keyword arguments are the following:
abort if no program can be found. If `required` is set to `false`,
Meson continue even if none of the programs can be found. You can
then use the `.found()` method on the returned object to check
whether it was found or not.
whether it was found or not. Since *0.47.0* the value of a
[`feature`](Build-options.md#features) option can also be passed to the
`required` keyword argument.

- `native` *(since 0.43)* defines how this executable should be searched. By default
it is set to `false`, which causes Meson to first look for the
Expand Down Expand Up @@ -1526,6 +1530,8 @@ the following methods:
library is searched for in the system library directory
(e.g. /usr/lib). This can be overridden with the `dirs` keyword
argument, which can be either a string or a list of strings.
Since *0.47.0* the value of a [`feature`](Build-options.md#features) option
can also be passed to the `required` keyword argument.

- `first_supported_argument(list_of_strings)`, given a list of
strings, returns the first argument that passes the `has_argument`
Expand Down
8 changes: 8 additions & 0 deletions mesonbuild/coredata.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,11 @@ def validate_value(self, value, user_input=True):
', '.join(bad), ', '.join(self.choices)))
return newvalue

class UserFeatureOption(UserComboOption):
static_choices = ['enabled', 'disabled', 'auto']
def __init__(self, name, description, value, yielding=None):
super().__init__(name, description, self.static_choices, value, yielding)

# This class contains all data that must persist over multiple
# invocations of Meson. It is roughly the same thing as
# cmakecache.
Expand Down Expand Up @@ -380,6 +385,8 @@ def get_builtin_option_choices(optname):
return builtin_options[optname][2]
elif builtin_options[optname][0] == UserBooleanOption:
return [True, False]
elif builtin_options[optname][0] == UserFeatureOption:
return UserFeatureOption.static_choices
else:
return None
else:
Expand Down Expand Up @@ -502,6 +509,7 @@ def filter_builtin_options(args, original_args):
'stdsplit': [UserBooleanOption, 'Split stdout and stderr in test logs.', True],
'errorlogs': [UserBooleanOption, "Whether to print the logs from failing tests.", True],
'install_umask': [UserUmaskOption, 'Default umask to apply on permissions of installed files.', '022'],
'feature_options': [UserFeatureOption, "Override value of all 'auto' features.", 'auto'],
}

# Special prefix-dependent defaults for installation directories that reside in
Expand Down
130 changes: 105 additions & 25 deletions mesonbuild/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,62 @@ def __init__(self, obj):
def __repr__(self):
return '<Holder: {!r}>'.format(self.held_object)

class FeatureOptionHolder(InterpreterObject, ObjectHolder):
def __init__(self, env, option):
InterpreterObject.__init__(self)
ObjectHolder.__init__(self, option)
if option.value == 'auto':
self.value = env.coredata.get_builtin_option('feature_options')
else:
self.value = option.value
self.methods.update({'enabled': self.enabled_method,
'disabled': self.disabled_method,
'auto': self.auto_method,
})

def is_enabled(self):
return self.value == 'enabled'

def is_disabled(self):
return self.value == 'disabled'

def is_auto(self):
return self.value == 'auto'

@noPosargs
@permittedKwargs({})
def enabled_method(self, args, kwargs):
return self.is_enabled()

@noPosargs
@permittedKwargs({})
def disabled_method(self, args, kwargs):
return self.is_disabled()

@noPosargs
@permittedKwargs({})
def auto_method(self, args, kwargs):
return self.is_auto()

def extract_required_kwarg(kwargs):
val = kwargs.get('required', True)
disabled = False
required = False
if isinstance(val, FeatureOptionHolder):
if val.is_disabled():
disabled = True
elif val.is_enabled():
required = True
elif isinstance(required, bool):
required = val
else:
raise InterpreterException('required keyword argument must be boolean or a feature option')

# Keep boolean value in kwargs to simplify other places where this kwarg is
# checked.
kwargs['required'] = required

return disabled, required

class TryRunResultHolder(InterpreterObject):
def __init__(self, res):
Expand Down Expand Up @@ -1291,9 +1347,15 @@ def find_library_method(self, args, kwargs):
libname = args[0]
if not isinstance(libname, str):
raise InterpreterException('Library name not a string.')
required = kwargs.get('required', True)
if not isinstance(required, bool):
raise InterpreterException('required must be boolean.')

disabled, required = extract_required_kwarg(kwargs)
if disabled:
lib = dependencies.ExternalLibrary(libname, None,
self.environment,
self.compiler.language,
silent=True)
return ExternalLibraryHolder(lib)

search_dirs = mesonlib.stringlistify(kwargs.get('dirs', []))
for i in search_dirs:
if not os.path.isabs(i):
Expand Down Expand Up @@ -2106,35 +2168,28 @@ def do_subproject(self, dirname, kwargs):
self.build_def_files += subi.build_def_files
return self.subprojects[dirname]

@stringArgs
@noKwargs
def func_get_option(self, nodes, args, kwargs):
if len(args) != 1:
raise InterpreterException('Argument required for get_option.')
undecorated_optname = optname = args[0]
if ':' in optname:
raise InterpreterException('''Having a colon in option name is forbidden, projects are not allowed
to directly access options of other subprojects.''')
def get_option_internal(self, optname):
undecorated_optname = optname
try:
return self.environment.get_coredata().base_options[optname].value
return self.coredata.base_options[optname]
except KeyError:
pass
try:
return self.environment.coredata.get_builtin_option(optname)
except RuntimeError:
return self.coredata.builtins[optname]
except KeyError:
pass
try:
return self.environment.coredata.compiler_options[optname].value
return self.coredata.compiler_options[optname]
except KeyError:
pass
if not coredata.is_builtin_option(optname) and self.is_subproject():
optname = self.subproject + ':' + optname
try:
opt = self.environment.coredata.user_options[optname]
opt = self.coredata.user_options[optname]
if opt.yielding and ':' in optname:
# If option not present in superproject, keep the original.
opt = self.environment.coredata.user_options.get(undecorated_optname, opt)
return opt.value
opt = self.coredata.user_options.get(undecorated_optname, opt)
return opt
except KeyError:
pass
if optname.endswith('_link_args'):
Expand All @@ -2151,11 +2206,28 @@ def func_get_option(self, nodes, args, kwargs):
pass
# Some base options are not defined in some environments, return the default value.
try:
return compilers.base_options[optname].value
return compilers.base_options[optname]
except KeyError:
pass
raise InterpreterException('Tried to access unknown option "%s".' % optname)

@stringArgs
@noKwargs
def func_get_option(self, nodes, args, kwargs):
if len(args) != 1:
raise InterpreterException('Argument required for get_option.')
optname = args[0]
if ':' in optname:
raise InterpreterException('Having a colon in option name is forbidden, '
'projects are not allowed to directly access '
'options of other subprojects.')
opt = self.get_option_internal(optname)
if isinstance(opt, coredata.UserFeatureOption):
return FeatureOptionHolder(self.environment, opt)
elif isinstance(opt, coredata.UserOption):
return opt.value
return opt

@noKwargs
def func_configuration_data(self, node, args, kwargs):
if args:
Expand Down Expand Up @@ -2275,7 +2347,10 @@ def func_project(self, node, args, kwargs):
@permittedKwargs(permitted_kwargs['add_languages'])
@stringArgs
def func_add_languages(self, node, args, kwargs):
return self.add_languages(args, kwargs.get('required', True))
disabled, required = extract_required_kwarg(kwargs)
if disabled:
return False
return self.add_languages(args, required)

def get_message_string_arg(self, node):
# reduce arguments again to avoid flattening posargs
Expand Down Expand Up @@ -2535,7 +2610,11 @@ def find_program_impl(self, args, native=False, required=True, silent=True):
def func_find_program(self, node, args, kwargs):
if not args:
raise InterpreterException('No program name specified.')
required = kwargs.get('required', True)

disabled, required = extract_required_kwarg(kwargs)
if disabled:
return ExternalProgramHolder(dependencies.NonExistingExternalProgram())

if not isinstance(required, bool):
raise InvalidArguments('"required" argument must be a boolean.')
use_native = kwargs.get('native', False)
Expand Down Expand Up @@ -2628,11 +2707,12 @@ def _find_cached_fallback_dep(self, name, dirname, varname, wanted, required):
@permittedKwargs(permitted_kwargs['dependency'])
def func_dependency(self, node, args, kwargs):
self.validate_arguments(args, 1, [str])
required = kwargs.get('required', True)
if not isinstance(required, bool):
raise DependencyException('Keyword "required" must be a boolean.')
name = args[0]

disabled, required = extract_required_kwarg(kwargs)
if disabled:
return DependencyHolder(Dependency(name, {}))

if name == '':
if required:
raise InvalidArguments('Dependency is both required and not-found')
Expand Down
3 changes: 2 additions & 1 deletion mesonbuild/mconf.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ def print_conf(self):
print(' Build dir ', self.build.environment.build_dir)
print('\nCore options:\n')
carr = []
for key in ['buildtype', 'warning_level', 'werror', 'strip', 'unity', 'default_library', 'install_umask']:
for key in ['buildtype', 'warning_level', 'werror', 'strip', 'unity',
'default_library', 'install_umask', 'feature_options']:
carr.append({'name': key,
'descr': coredata.get_builtin_option_description(key),
'value': self.coredata.get_builtin_option(key),
Expand Down
8 changes: 8 additions & 0 deletions mesonbuild/optinterpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,19 @@ def string_array_parser(name, description, kwargs):
choices=choices,
yielding=kwargs.get('yield', coredata.default_yielding))

@permitted_kwargs({'value', 'yield'})
def FeatureParser(name, description, kwargs):
return coredata.UserFeatureOption(name,
description,
kwargs.get('value', 'enabled'),
yielding=kwargs.get('yield', coredata.default_yielding))

option_types = {'string': StringParser,
'boolean': BooleanParser,
'combo': ComboParser,
'integer': IntegerParser,
'array': string_array_parser,
'feature': FeatureParser,
}

class OptionInterpreter:
Expand Down
47 changes: 47 additions & 0 deletions test cases/common/200 feature option/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
project('feature user option', 'c')

feature_opts = get_option('feature_options')
required_opt = get_option('required')
optional_opt = get_option('optional')
disabled_opt = get_option('disabled')

assert(not feature_opts.enabled(), 'Should be auto option')
assert(not feature_opts.disabled(), 'Should be auto option')
assert(feature_opts.auto(), 'Should be auto option')

assert(required_opt.enabled(), 'Should be enabled option')
assert(not required_opt.disabled(), 'Should be enabled option')
assert(not required_opt.auto(), 'Should be enabled option')

assert(not optional_opt.enabled(), 'Should be auto option')
assert(not optional_opt.disabled(), 'Should be auto option')
assert(optional_opt.auto(), 'Should be auto option')

assert(not disabled_opt.enabled(), 'Should be disabled option')
assert(disabled_opt.disabled(), 'Should be disabled option')
assert(not disabled_opt.auto(), 'Should be disabled option')

dep = dependency('threads', required : required_opt)
assert(dep.found(), 'Should find required "threads" dep')

dep = dependency('threads', required : optional_opt)
assert(dep.found(), 'Should find optional "threads" dep')

dep = dependency('threads', required : disabled_opt)
assert(not dep.found(), 'Should not find disabled "threads" dep')

dep = dependency('notfounddep', required : optional_opt)
assert(not dep.found(), 'Should not find optional "notfounddep" dep')

dep = dependency('notfounddep', required : disabled_opt)
assert(not dep.found(), 'Should not find disabled "notfounddep" dep')

cc = meson.get_compiler('c')
lib = cc.find_library('m', required : disabled_opt)
assert(not lib.found(), 'Should not find "m" library')

cp = find_program('cp', required : disabled_opt)
assert(not cp.found(), 'Should not find "cp" program')

found = add_languages('cpp', required : disabled_opt)
assert(not found, 'Should not find "cpp" language')
3 changes: 3 additions & 0 deletions test cases/common/200 feature option/meson_options.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
option('required', type : 'feature', value : 'enabled', description : 'An required feature')
option('optional', type : 'feature', value : 'auto', description : 'An optional feature')
option('disabled', type : 'feature', value : 'disabled', description : 'A disabled feature')

0 comments on commit ad3faab

Please sign in to comment.