| # 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 coredata |
| from . import options |
| from . import mesonlib |
| 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 .interpreterbase import SubProject |
| from typing_extensions import TypedDict, Literal |
| |
| _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, subproject: 'SubProject') -> None: |
| self.options: 'coredata.MutableKeyedOptionDictType' = {} |
| self.subproject = subproject |
| self.option_types: T.Dict[str, T.Callable[..., options.UserOption]] = { |
| '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: |
| 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 == '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.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 = mesonlib.OptionKey.from_string(opt_name).evolve(subproject=self.subproject) |
| if not key.is_project(): |
| 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: |
| return options.UserBooleanOption(name, description, kwargs['value'], *args) |
| |
| @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, choices, value, *args) |
| |
| @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: |
| value = kwargs['value'] |
| inttuple = (kwargs['min'], kwargs['max'], value) |
| return options.UserIntegerOption(name, description, inttuple, *args) |
| |
| @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.UserArrayOption(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) |