From ad3faab62dc66687081356309a00d310f19dae50 Mon Sep 17 00:00:00 2001 From: Xavier Claessens Date: Wed, 11 Apr 2018 10:13:14 -0400 Subject: [PATCH] Add UserFeatureOption type 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. --- docs/markdown/Build-options.md | 31 +++++ docs/markdown/Reference-manual.md | 12 +- mesonbuild/coredata.py | 8 ++ mesonbuild/interpreter.py | 130 ++++++++++++++---- mesonbuild/mconf.py | 3 +- mesonbuild/optinterpreter.py | 8 ++ .../common/200 feature option/meson.build | 47 +++++++ .../200 feature option/meson_options.txt | 3 + .../201 feature option disabled/meson.build | 23 ++++ .../meson_options.txt | 3 + 10 files changed, 239 insertions(+), 29 deletions(-) create mode 100644 test cases/common/200 feature option/meson.build create mode 100644 test cases/common/200 feature option/meson_options.txt create mode 100644 test cases/common/201 feature option disabled/meson.build create mode 100644 test cases/common/201 feature option disabled/meson_options.txt diff --git a/docs/markdown/Build-options.md b/docs/markdown/Build-options.md index 9ccdf8391615..5c732517836e 100644 --- a/docs/markdown/Build-options.md +++ b/docs/markdown/Build-options.md @@ -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, @@ -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 diff --git a/docs/markdown/Reference-manual.md b/docs/markdown/Reference-manual.md index 2f45451412c3..aa3b6f0de7de 100644 --- a/docs/markdown/Reference-manual.md +++ b/docs/markdown/Reference-manual.md @@ -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() @@ -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) @@ -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 @@ -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` diff --git a/mesonbuild/coredata.py b/mesonbuild/coredata.py index f3313f3f9e45..6d2f2dcbff42 100644 --- a/mesonbuild/coredata.py +++ b/mesonbuild/coredata.py @@ -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. @@ -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: @@ -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 diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index c5744795e9cd..dd02ddc5dd40 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -61,6 +61,62 @@ def __init__(self, obj): def __repr__(self): return ''.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): @@ -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): @@ -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'): @@ -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: @@ -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 @@ -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) @@ -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') diff --git a/mesonbuild/mconf.py b/mesonbuild/mconf.py index f65d6ff8eb65..7b82b3c8dc9a 100644 --- a/mesonbuild/mconf.py +++ b/mesonbuild/mconf.py @@ -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), diff --git a/mesonbuild/optinterpreter.py b/mesonbuild/optinterpreter.py index b4156ffe0b65..3274e5668fda 100644 --- a/mesonbuild/optinterpreter.py +++ b/mesonbuild/optinterpreter.py @@ -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: diff --git a/test cases/common/200 feature option/meson.build b/test cases/common/200 feature option/meson.build new file mode 100644 index 000000000000..3e22ebbc9d15 --- /dev/null +++ b/test cases/common/200 feature option/meson.build @@ -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') diff --git a/test cases/common/200 feature option/meson_options.txt b/test cases/common/200 feature option/meson_options.txt new file mode 100644 index 000000000000..063a35f39c99 --- /dev/null +++ b/test cases/common/200 feature option/meson_options.txt @@ -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') diff --git a/test cases/common/201 feature option disabled/meson.build b/test cases/common/201 feature option disabled/meson.build new file mode 100644 index 000000000000..eebeaad608bc --- /dev/null +++ b/test cases/common/201 feature option disabled/meson.build @@ -0,0 +1,23 @@ +project('feature user option', 'c', + default_options : ['feature_options=disabled']) + +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 disabled option') +assert(feature_opts.disabled(), 'Should be disabled option') +assert(not feature_opts.auto(), 'Should be disabled 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(), 'Auto feature should be disabled') +assert(optional_opt.disabled(), 'Auto feature should be disabled') +assert(not optional_opt.auto(), 'Auto feature should be disabled') + +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') diff --git a/test cases/common/201 feature option disabled/meson_options.txt b/test cases/common/201 feature option disabled/meson_options.txt new file mode 100644 index 000000000000..063a35f39c99 --- /dev/null +++ b/test cases/common/201 feature option disabled/meson_options.txt @@ -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')