mirror of
https://github.com/mesonbuild/meson.git
synced 2026-06-30 19:57:45 +00:00
Since we use it at ever layer of the codebase, it doesn't make sense for it to live in the interpreter
284 lines
12 KiB
Python
284 lines
12 KiB
Python
# 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)
|