# 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 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[..., coredata.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: 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 if isinstance(arg, mparser.ParenthesizedNode): return self.reduce_single(arg.inner) elif isinstance(arg, (mparser.BaseStringNode, 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.BaseStringNode): 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=coredata.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(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, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: StringArgs) -> coredata.UserOption: return coredata.UserStringOption(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, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: BooleanArgs) -> coredata.UserOption: return coredata.UserBooleanOption(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, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: ComboArgs) -> coredata.UserOption: choices = kwargs['choices'] value = kwargs['value'] if value is None: value = kwargs['choices'][0] return coredata.UserComboOption(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, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: IntegerArgs) -> coredata.UserOption: value = kwargs['value'] inttuple = (kwargs['min'], kwargs['max'], value) return coredata.UserIntegerOption(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, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: StringArrayArgs) -> coredata.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 coredata.UserArrayOption(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, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: FeatureArgs) -> coredata.UserOption: return coredata.UserFeatureOption(description, kwargs['value'], *args)