| # SPDX-License-Identifier: Apache-2.0 | |
| # Copyright 2013-2014 The Meson development team | |
| from __future__ import annotations | |
| import re | |
| import typing as T | |
| from . import options | |
| from . import mesonlib | |
| from .options import OptionKey | |
| from . import mparser | |
| from . import mlog | |
| from .interpreterbase import FeatureNew, FeatureDeprecated, typed_pos_args, typed_kwargs, ContainerTypeInfo, KwargInfo | |
| from .interpreter.type_checking import NoneType, in_set_validator | |
| if T.TYPE_CHECKING: | |
| from .interpreterbase import TYPE_var, TYPE_kwargs | |
| from .mesonlib import SubProject | |
| from typing_extensions import TypedDict, Literal | |
| from .options import OptionStore | |
| _DEPRECATED_ARGS = T.Union[bool, str, T.Dict[str, str], T.List[str]] | |
| 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': _DEPRECATED_ARGS, | |
| }) | |
| class StringArgs(TypedDict): | |
| value: str | |
| class BooleanArgs(TypedDict): | |
| value: bool | |
| class ComboArgs(TypedDict): | |
| value: str | |
| choices: T.List[str] | |
| class IntegerArgs(TypedDict): | |
| value: int | |
| min: T.Optional[int] | |
| max: T.Optional[int] | |
| class StringArrayArgs(TypedDict): | |
| value: T.Optional[T.Union[str, T.List[str]]] | |
| choices: T.List[str] | |
| class FeatureArgs(TypedDict): | |
| value: Literal['enabled', 'disabled', 'auto'] | |
| choices: T.List[str] | |
| class OptionException(mesonlib.MesonException): | |
| pass | |
| optname_regex = re.compile('[^a-zA-Z0-9_-]') | |
| class OptionInterpreter: | |
| def __init__(self, optionstore: 'OptionStore', subproject: 'SubProject') -> None: | |
| self.options: options.MutableKeyedOptionDictType = {} | |
| self.subproject = subproject | |
| self.option_types: T.Dict[str, T.Callable[..., options.AnyOptionType]] = { | |
| '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, | |
| } | |
| self.optionstore = optionstore | |
| def process(self, option_file: str) -> None: | |
| try: | |
| with open(option_file, encoding='utf-8') as f: | |
| code = f.read() | |
| except UnicodeDecodeError as e: | |
| raise mesonlib.MesonException(f'Malformed option file {option_file!r} failed to parse as unicode: {e}') | |
| try: | |
| ast = mparser.Parser(code, 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 | |
| if isinstance(arg, mparser.ParenthesizedNode): | |
| return self.reduce_single(arg.inner) | |
| 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 == '+' 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.value | |
| 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, | |
| validator=in_set_validator({'string', 'boolean', 'integer', 'combo', 'array', 'feature'}) | |
| ), | |
| KwargInfo('description', str, default=''), | |
| KwargInfo( | |
| 'deprecated', | |
| (bool, str, ContainerTypeInfo(dict, str), ContainerTypeInfo(list, str)), | |
| default=False, | |
| since='0.60.0', | |
| since_values={str: '0.63.0'}, | |
| ), | |
| KwargInfo('yield', bool, default=options.DEFAULT_YIELDING, since='0.45.0'), | |
| allow_unknown=True, | |
| ) | |
| @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.') | |
| key = OptionKey.from_string(opt_name).evolve(subproject=self.subproject) | |
| if self.optionstore.is_reserved_name(key): | |
| raise OptionException('Option name %s is reserved.' % opt_name) | |
| opt_type = kwargs['type'] | |
| parser = self.option_types[opt_type] | |
| description = kwargs['description'] or opt_name | |
| # Drop the arguments we've already consumed | |
| n_kwargs = {k: v for k, v in kwargs.items() | |
| if k not in {'type', 'description', 'deprecated', 'yield'}} | |
| opt = parser(opt_name, description, (kwargs['yield'], kwargs['deprecated']), n_kwargs) | |
| if key in self.options: | |
| mlog.deprecation(f'Option {opt_name} already exists.') | |
| self.options[key] = opt | |
| @typed_kwargs( | |
| 'string option', | |
| KwargInfo('value', str, default=''), | |
| ) | |
| def string_parser(self, name: str, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: StringArgs) -> options.UserOption: | |
| return options.UserStringOption(name, description, kwargs['value'], *args) | |
| @typed_kwargs( | |
| 'boolean option', | |
| KwargInfo( | |
| 'value', | |
| (bool, str), | |
| default=True, | |
| validator=lambda x: None if isinstance(x, bool) or x in {'true', 'false'} else 'boolean options must have boolean values', | |
| deprecated_values={str: ('1.1.0', 'use a boolean, not a string')}, | |
| ), | |
| ) | |
| def boolean_parser(self, name: str, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: BooleanArgs) -> options.UserOption: | |
| yielding, deprecated = args | |
| return options.UserBooleanOption(name, description, kwargs['value'], yielding=yielding, deprecated=deprecated) | |
| @typed_kwargs( | |
| 'combo option', | |
| KwargInfo('value', (str, NoneType)), | |
| KwargInfo('choices', ContainerTypeInfo(list, str, allow_empty=False), required=True), | |
| ) | |
| def combo_parser(self, name: str, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: ComboArgs) -> options.UserOption: | |
| choices = kwargs['choices'] | |
| value = kwargs['value'] | |
| if value is None: | |
| value = kwargs['choices'][0] | |
| return options.UserComboOption(name, description, value, *args, choices=choices) | |
| @typed_kwargs( | |
| 'integer option', | |
| KwargInfo( | |
| 'value', | |
| (int, str), | |
| default=True, | |
| deprecated_values={str: ('1.1.0', 'use an integer, not a string')}, | |
| convertor=int, | |
| ), | |
| KwargInfo('min', (int, NoneType)), | |
| KwargInfo('max', (int, NoneType)), | |
| ) | |
| def integer_parser(self, name: str, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: IntegerArgs) -> options.UserOption: | |
| return options.UserIntegerOption( | |
| name, description, kwargs['value'], *args, min_value=kwargs['min'], max_value=kwargs['max']) | |
| @typed_kwargs( | |
| 'string array option', | |
| KwargInfo('value', (ContainerTypeInfo(list, str), str, NoneType)), | |
| KwargInfo('choices', ContainerTypeInfo(list, str), default=[]), | |
| ) | |
| def string_array_parser(self, name: str, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: StringArrayArgs) -> options.UserOption: | |
| choices = kwargs['choices'] | |
| value = kwargs['value'] if kwargs['value'] is not None else choices | |
| if isinstance(value, str): | |
| if value.startswith('['): | |
| FeatureDeprecated('String value for array option', '1.3.0').use(self.subproject) | |
| else: | |
| raise mesonlib.MesonException('Value does not define an array: ' + value) | |
| return options.UserStringArrayOption( | |
| name, description, value, | |
| choices=choices, | |
| yielding=args[0], | |
| deprecated=args[1]) | |
| @typed_kwargs( | |
| 'feature option', | |
| KwargInfo('value', str, default='auto', validator=in_set_validator({'auto', 'enabled', 'disabled'})), | |
| ) | |
| def feature_parser(self, name: str, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: FeatureArgs) -> options.UserOption: | |
| return options.UserFeatureOption(name, description, kwargs['value'], *args) |