# Copyright 2013-2014 The Meson development team # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import re import typing as T from . import compilers from . import coredata from . import mesonlib from . import mparser from . import mlog from .interpreterbase import FeatureNew, typed_pos_args, typed_kwargs, ContainerTypeInfo, KwargInfo, permittedKwargs if T.TYPE_CHECKING: from .interpreterbase import TV_func, TYPE_var, TYPE_kwargs from typing_extensions import TypedDict FuncOptionArgs = TypedDict('FuncOptionArgs', { 'type': str, 'description': str, 'yield': bool, 'choices': T.Optional[T.List[str]], 'value': object, 'min': T.Optional[int], 'max': T.Optional[int], 'deprecated': T.Union[bool, T.Dict[str, str], T.List[str]], }) ParserArgs = TypedDict('ParserArgs', { 'yield': bool, 'choices': T.Optional[T.List[str]], 'value': object, 'min': T.Optional[int], 'max': T.Optional[int], }) forbidden_option_names = set(coredata.BUILTIN_OPTIONS.keys()) forbidden_prefixes = [lang + '_' for lang in compilers.all_languages] + ['b_', 'backend_'] reserved_prefixes = ['cross_'] def is_invalid_name(name: str, *, log: bool = True) -> bool: if name in forbidden_option_names: return True pref = name.split('_')[0] + '_' if pref in forbidden_prefixes: return True if pref in reserved_prefixes: if log: mlog.deprecation('Option uses prefix "%s", which is reserved for Meson. This will become an error in the future.' % pref) return False class OptionException(mesonlib.MesonException): pass optname_regex = re.compile('[^a-zA-Z0-9_-]') class OptionInterpreter: def __init__(self, subproject: str) -> None: self.options: 'coredata.KeyedOptionDictType' = {} self.subproject = subproject self.option_types = {'string': self.string_parser, 'boolean': self.boolean_parser, 'combo': self.combo_parser, 'integer': self.integer_parser, 'array': self.string_array_parser, 'feature': self.feature_parser, } def process(self, option_file: str) -> None: try: with open(option_file, encoding='utf-8') as f: ast = mparser.Parser(f.read(), option_file).parse() except mesonlib.MesonException as me: me.file = option_file raise me if not isinstance(ast, mparser.CodeBlockNode): e = OptionException('Option file is malformed.') e.lineno = ast.lineno() e.file = option_file raise e for cur in ast.lines: try: self.current_node = cur self.evaluate_statement(cur) except mesonlib.MesonException as e: e.lineno = cur.lineno e.colno = cur.colno e.file = option_file raise e except Exception as e: raise mesonlib.MesonException( str(e), lineno=cur.lineno, colno=cur.colno, file=option_file) def reduce_single(self, arg: T.Union[str, mparser.BaseNode]) -> 'TYPE_var': if isinstance(arg, str): return arg elif isinstance(arg, (mparser.StringNode, mparser.BooleanNode, mparser.NumberNode)): return arg.value elif isinstance(arg, mparser.ArrayNode): return [self.reduce_single(curarg) for curarg in arg.args.arguments] elif isinstance(arg, mparser.DictNode): d = {} for k, v in arg.args.kwargs.items(): if not isinstance(k, mparser.StringNode): raise OptionException('Dictionary keys must be a string literal') d[k.value] = self.reduce_single(v) return d elif isinstance(arg, mparser.UMinusNode): res = self.reduce_single(arg.value) if not isinstance(res, (int, float)): raise OptionException('Token after "-" is not a number') FeatureNew.single_use('negative numbers in meson_options.txt', '0.54.1', self.subproject) return -res elif isinstance(arg, mparser.NotNode): res = self.reduce_single(arg.value) if not isinstance(res, bool): raise OptionException('Token after "not" is not a a boolean') FeatureNew.single_use('negation ("not") in meson_options.txt', '0.54.1', self.subproject) return not res elif isinstance(arg, mparser.ArithmeticNode): l = self.reduce_single(arg.left) r = self.reduce_single(arg.right) if not (arg.operation == 'add' and isinstance(l, str) and isinstance(r, str)): raise OptionException('Only string concatenation with the "+" operator is allowed') FeatureNew.single_use('string concatenation in meson_options.txt', '0.55.0', self.subproject) return l + r else: raise OptionException('Arguments may only be string, int, bool, or array of those.') def reduce_arguments(self, args: mparser.ArgumentNode) -> T.Tuple['TYPE_var', 'TYPE_kwargs']: if args.incorrect_order(): raise OptionException('All keyword arguments must be after positional arguments.') reduced_pos = [self.reduce_single(arg) for arg in args.arguments] reduced_kw = {} for key in args.kwargs.keys(): if not isinstance(key, mparser.IdNode): raise OptionException('Keyword argument name is not a string.') a = args.kwargs[key] reduced_kw[key.value] = self.reduce_single(a) return reduced_pos, reduced_kw def evaluate_statement(self, node: mparser.BaseNode) -> None: 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) self.func_option(posargs, kwargs) @typed_kwargs('option', KwargInfo('type', str, required=True), KwargInfo('description', str, default=''), KwargInfo('yield', bool, default=coredata.default_yielding, since='0.45.0'), KwargInfo('choices', (ContainerTypeInfo(list, str), type(None))), KwargInfo('value', object), KwargInfo('min', (int, type(None))), KwargInfo('max', (int, type(None))), KwargInfo('deprecated', (bool, ContainerTypeInfo(dict, str), ContainerTypeInfo(list, str)), default=False, since='0.60.0') ) @typed_pos_args('option', str) def func_option(self, args: T.Tuple[str], kwargs: 'FuncOptionArgs') -> None: opt_name = args[0] if optname_regex.search(opt_name) is not None: 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) opt_type = kwargs['type'] parser = self.option_types.get(opt_type) if not parser: raise OptionException(f'Unknown type {opt_type}.') description = kwargs['description'] or opt_name # Only keep in kwargs arguments that are used by option type's parser # because they use @permittedKwargs(). known_parser_kwargs = {'value', 'choices', 'yield', 'min', 'max'} parser_kwargs = {k: v for k, v in kwargs.items() if k in known_parser_kwargs and v is not None} opt = parser(description, T.cast('ParserArgs', parser_kwargs)) opt.deprecated = kwargs['deprecated'] key = mesonlib.OptionKey(opt_name, self.subproject) if key in self.options: raise OptionException(f'Option {opt_name} already exists') self.options[key] = opt @permittedKwargs({'value', 'yield'}) def string_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption: value = kwargs.get('value', '') return coredata.UserStringOption(description, value, kwargs['yield']) @permittedKwargs({'value', 'yield'}) def boolean_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption: value = kwargs.get('value', True) return coredata.UserBooleanOption(description, value, kwargs['yield']) @permittedKwargs({'value', 'yield', 'choices'}) def combo_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption: choices = kwargs.get('choices') if not choices: raise OptionException('Combo option missing "choices" keyword.') value = kwargs.get('value', choices[0]) return coredata.UserComboOption(description, choices, value, kwargs['yield']) @permittedKwargs({'value', 'min', 'max', 'yield'}) def integer_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption: value = kwargs.get('value') if value is None: raise OptionException('Integer option must contain value argument.') inttuple = (kwargs.get('min'), kwargs.get('max'), value) return coredata.UserIntegerOption(description, inttuple, kwargs['yield']) @permittedKwargs({'value', 'yield', 'choices'}) def string_array_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption: choices = kwargs.get('choices', []) value = kwargs.get('value', choices) if not isinstance(value, list): raise OptionException('Array choices must be passed as an array.') return coredata.UserArrayOption(description, value, choices=choices, yielding=kwargs['yield']) @permittedKwargs({'value', 'yield'}) def feature_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption: value = kwargs.get('value', 'auto') return coredata.UserFeatureOption(description, value, kwargs['yield'])