diff --git a/mesonbuild/coredata.py b/mesonbuild/coredata.py index 2f1875f14..f60cc7beb 100644 --- a/mesonbuild/coredata.py +++ b/mesonbuild/coredata.py @@ -18,19 +18,26 @@ from .mesonlib import MesonException, default_libdir, default_libexecdir, defaul version = '0.34.0.dev1' backendlist = ['ninja', 'vs2010', 'vs2015', 'xcode'] +class SubOption: + def __init__(self, name, parent, description): + self.name = name + self.parent = parent + self.description = description + class UserOption: - def __init__(self, name, description, choices): + def __init__(self, name, description, choices, parent=None): super().__init__() self.name = name self.choices = choices self.description = description + self.parent = parent def parse_string(self, valuestring): return valuestring class UserStringOption(UserOption): - def __init__(self, name, description, value, choices=None): - super().__init__(name, description, choices) + def __init__(self, name, description, value, parent=None, choices=None): + super().__init__(name, description, choices, parent) self.set_value(value) def validate(self, value): @@ -47,8 +54,8 @@ class UserStringOption(UserOption): self.value = newvalue class UserBooleanOption(UserOption): - def __init__(self, name, description, value): - super().__init__(name, description, [ True, False ]) + def __init__(self, name, description, value, parent=None): + super().__init__(name, description, [ True, False ], parent) self.set_value(value) def tobool(self, thing): @@ -74,8 +81,8 @@ class UserBooleanOption(UserOption): return self.value class UserComboOption(UserOption): - def __init__(self, name, description, choices, value): - super().__init__(name, description, choices) + def __init__(self, name, description, choices, value, parent=None): + super().__init__(name, description, choices, parent) if not isinstance(self.choices, list): raise MesonException('Combo choices must be an array.') for i in self.choices: @@ -90,8 +97,8 @@ class UserComboOption(UserOption): self.value = newvalue class UserStringArrayOption(UserOption): - def __init__(self, name, description, value, **kwargs): - super().__init__(name, description, kwargs.get('choices', [])) + def __init__(self, name, description, value, parent=None, **kwargs): + super().__init__(name, description, kwargs.get('choices', []), parent) self.set_value(value) def set_value(self, newvalue): @@ -120,6 +127,7 @@ class CoreData(): self.version = version self.init_builtins(options) self.user_options = {} + self.suboptions = {} # In GUIs these would be called "submenus". self.compiler_options = {} self.base_options = {} self.external_args = {} # These are set from "the outside" with e.g. mesonconf diff --git a/mesonbuild/environment.py b/mesonbuild/environment.py index 341e5e8fc..9848e699e 100644 --- a/mesonbuild/environment.py +++ b/mesonbuild/environment.py @@ -216,7 +216,9 @@ class Environment(): previous_is_plaind = i == '-D' return False - def merge_options(self, options): + def merge_options(self, options, suboptions): + for (name, value) in suboptions.items(): + self.coredata.suboptions[name] = value for (name, value) in options.items(): if name not in self.coredata.user_options: self.coredata.user_options[name] = value diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index c9a81fb87..f48cc5874 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -1028,7 +1028,7 @@ class Interpreter(): oi = optinterpreter.OptionInterpreter(self.subproject, \ self.build.environment.cmd_line_options.projectoptions) oi.process(option_file) - self.build.environment.merge_options(oi.options) + self.build.environment.merge_options(oi.options, oi.suboptions) mesonfile = os.path.join(self.source_root, self.subdir, environment.build_filename) if not os.path.isfile(mesonfile): raise InvalidArguments('Missing Meson file in %s' % mesonfile) diff --git a/mesonbuild/mintro.py b/mesonbuild/mintro.py index 629b0fcd3..14f82a6e8 100644 --- a/mesonbuild/mintro.py +++ b/mesonbuild/mintro.py @@ -99,34 +99,71 @@ def list_buildoptions(coredata, builddata): 'type' : 'boolean', 'description' : 'Unity build', 'name' : 'unity'} - optlist = [buildtype, strip, unity] - add_keys(optlist, coredata.user_options) - add_keys(optlist, coredata.compiler_options) - add_keys(optlist, coredata.base_options) - print(json.dumps(optlist)) + all_opt = [] + all_opt.append({'name' : 'core', + 'description' : 'Core options', + 'type' : 'suboption', + 'value' : [buildtype, strip, unity], + }) + all_opt.append({'name': 'user', + 'description' : 'User defined options', + 'type' : 'suboption', + 'value' : build_usertree(coredata.suboptions, coredata.user_options), + }) + all_opt.append({'name' : 'compilers', + 'description' : 'Options for compilers', + 'type' : 'suboption', + 'value' : get_keys(coredata.compiler_options), + }) + all_opt.append({'name': 'base', + 'description' : 'Base options', + 'type' : 'suboption', + 'value' : get_keys(coredata.base_options), + }) + print(json.dumps(all_opt, indent=2)) + +def build_usertree(suboptions, user_options, subbranch=None): + current = [] + current_suboptions = [x for x in suboptions.values() if x.parent == subbranch] + current_options = [x for x in user_options.values() if x.parent == subbranch] + for so in current_suboptions: + subentry = {'type' : 'subobject', + 'value' : build_usertree(suboptions, user_options, so.name), + 'description' : so.description, + 'name' : so.name + } + current.append(subentry) + for opt in current_options: + current.append(opt2dict(opt.name, opt)) + return current + +def opt2dict(key, opt): + optdict = {} + optdict['name'] = key + optdict['value'] = opt.value + if isinstance(opt, coredata.UserStringOption): + typestr = 'string' + elif isinstance(opt, coredata.UserBooleanOption): + typestr = 'boolean' + elif isinstance(opt, coredata.UserComboOption): + optdict['choices'] = opt.choices + typestr = 'combo' + elif isinstance(opt, coredata.UserStringArrayOption): + typestr = 'stringarray' + else: + raise RuntimeError("Unknown option type") + optdict['type'] = typestr + optdict['description'] = opt.description + return optdict -def add_keys(optlist, options): +def get_keys(options): + optlist = [] keys = list(options.keys()) keys.sort() for key in keys: opt = options[key] - optdict = {} - optdict['name'] = key - optdict['value'] = opt.value - if isinstance(opt, coredata.UserStringOption): - typestr = 'string' - elif isinstance(opt, coredata.UserBooleanOption): - typestr = 'boolean' - elif isinstance(opt, coredata.UserComboOption): - optdict['choices'] = opt.choices - typestr = 'combo' - elif isinstance(opt, coredata.UserStringArrayOption): - typestr = 'stringarray' - else: - raise RuntimeError("Unknown option type") - optdict['type'] = typestr - optdict['description'] = opt.description - optlist.append(optdict) + optlist.append(opt2dict(key, opt)) + return optlist def list_buildsystem_files(coredata, builddata): src_dir = builddata.environment.get_source_dir() diff --git a/mesonbuild/optinterpreter.py b/mesonbuild/optinterpreter.py index b35504747..2cd2df2f8 100644 --- a/mesonbuild/optinterpreter.py +++ b/mesonbuild/optinterpreter.py @@ -44,14 +44,14 @@ class OptionException(mesonlib.MesonException): optname_regex = re.compile('[^a-zA-Z0-9_-]') -def StringParser(name, description, kwargs): +def StringParser(name, description, parent, kwargs): return coredata.UserStringOption(name, description, - kwargs.get('value', ''), kwargs.get('choices', [])) + kwargs.get('value', ''), kwargs.get('choices', []), parent) -def BooleanParser(name, description, kwargs): - return coredata.UserBooleanOption(name, description, kwargs.get('value', True)) +def BooleanParser(name, description, parent, kwargs): + return coredata.UserBooleanOption(name, description, kwargs.get('value', True), parent) -def ComboParser(name, description, kwargs): +def ComboParser(name, description, parent, kwargs): if 'choices' not in kwargs: raise OptionException('Combo option missing "choices" keyword.') choices = kwargs['choices'] @@ -60,7 +60,7 @@ def ComboParser(name, description, kwargs): for i in choices: if not isinstance(i, str): raise OptionException('Combo choice elements must be strings.') - return coredata.UserComboOption(name, description, choices, kwargs.get('value', choices[0])) + return coredata.UserComboOption(name, description, choices, kwargs.get('value', choices[0]), parent) option_types = {'string' : StringParser, 'boolean' : BooleanParser, @@ -70,6 +70,7 @@ option_types = {'string' : StringParser, class OptionInterpreter: def __init__(self, subproject, command_line_options): self.options = {} + self.suboptions = {} self.subproject = subproject self.cmd_line_options = {} for o in command_line_options: @@ -123,16 +124,35 @@ class OptionInterpreter: if not isinstance(node, mparser.FunctionNode): raise OptionException('Option file may only contain option definitions') func_name = node.func_name - if func_name != 'option': - raise OptionException('Only calls to option() are allowed in option files.') (posargs, kwargs) = self.reduce_arguments(node.args) + if len(posargs) != 1: + raise OptionException('Function call must have one (and only one) positional argument') + if func_name == 'option': + return self.evaluate_option(posargs, kwargs) + elif func_name == 'suboption': + return self.evaluate_suboption(posargs, kwargs) + else: + raise OptionException('Only calls to option() or suboption() are allowed in option files.') + + def evaluate_suboption(self, posargs, kwargs): + subopt_name = posargs[0] + parent_name = kwargs.get('parent', None) + if self.subproject != '': + subopt_name = self.subproject + ':' + subopt_name + if parent_name is not None: + parent_name = self.subproject + ':' + parent_name + if subopt_name in self.suboptions: + raise OptionException('Tried to redefine suboption %s.' % subopt_name) + description = kwargs.get('description', subopt_name) + so = coredata.SubOption(subopt_name, parent_name, description) + self.suboptions[subopt_name] = so + + def evaluate_option(self, posargs, kwargs): if 'type' not in kwargs: raise OptionException('Option call missing mandatory "type" keyword argument') opt_type = kwargs['type'] if not opt_type in option_types: raise OptionException('Unknown type %s.' % opt_type) - if len(posargs) != 1: - raise OptionException('Option() must have one (and only one) positional argument') opt_name = posargs[0] if not isinstance(opt_name, str): raise OptionException('Positional argument must be a string.') @@ -140,9 +160,19 @@ class OptionInterpreter: raise OptionException('Option names can only contain letters, numbers or dashes.') if is_invalid_name(opt_name): raise OptionException('Option name %s is reserved.' % opt_name) + parent = kwargs.get('parent', None) + if parent is not None: + if not isinstance(parent, str): + raise OptionException('Parent, if set, must be a string.') if self.subproject != '': opt_name = self.subproject + ':' + opt_name - opt = option_types[opt_type](opt_name, kwargs.get('description', ''), kwargs) + if parent is not None: + parent = self.subproject + ':' + parent + if opt_name in self.options: + raise OptionException('Tried to redeclare option named %s.' % opt_name) + if parent is not None and parent not in self.suboptions: + raise OptionException('Parent %s of option %s is unknown.' % (parent, opt_name)) + opt = option_types[opt_type](opt_name, kwargs.get('description', ''), parent, kwargs) if opt.description == '': opt.description = opt_name if opt_name in self.cmd_line_options: diff --git a/test cases/common/47 options/meson_options.txt b/test cases/common/47 options/meson_options.txt index 653dd75f9..f65da8642 100644 --- a/test cases/common/47 options/meson_options.txt +++ b/test cases/common/47 options/meson_options.txt @@ -1,3 +1,7 @@ option('testoption', type : 'string', value : 'optval', description : 'An option to do something') option('other_one', type : 'boolean', value : false) option('combo_opt', type : 'combo', choices : ['one', 'two', 'combo'], value : 'combo') +suboption('suboptions') +option('subbool', type : 'boolean', parent : 'suboptions', description : 'An option in a submenu.') +suboption('subsuboptions', parent : 'suboptions') +option('subsubbool', type : 'boolean', parent : 'subsuboptions', description : 'A subsubmenuoption.')