optinterpreter.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. # Copyright 2013-2014 The Meson development team
  2. # Licensed under the Apache License, Version 2.0 (the "License");
  3. # you may not use this file except in compliance with the License.
  4. # You may obtain a copy of the License at
  5. # http://www.apache.org/licenses/LICENSE-2.0
  6. # Unless required by applicable law or agreed to in writing, software
  7. # distributed under the License is distributed on an "AS IS" BASIS,
  8. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. # See the License for the specific language governing permissions and
  10. # limitations under the License.
  11. import os, re
  12. import functools
  13. from . import mparser
  14. from . import coredata
  15. from . import mesonlib
  16. from . import compilers
  17. forbidden_option_names = coredata.get_builtin_options()
  18. forbidden_prefixes = [lang + '_' for lang in compilers.all_languages] + ['b_', 'backend_']
  19. def is_invalid_name(name):
  20. if name in forbidden_option_names:
  21. return True
  22. pref = name.split('_')[0] + '_'
  23. if pref in forbidden_prefixes:
  24. return True
  25. return False
  26. class OptionException(mesonlib.MesonException):
  27. pass
  28. def permitted_kwargs(permitted):
  29. """Function that validates kwargs for options."""
  30. def _wraps(func):
  31. @functools.wraps(func)
  32. def _inner(name, description, kwargs):
  33. bad = [a for a in kwargs.keys() if a not in permitted]
  34. if bad:
  35. raise OptionException('Invalid kwargs for option "{}": "{}"'.format(
  36. name, ' '.join(bad)))
  37. return func(name, description, kwargs)
  38. return _inner
  39. return _wraps
  40. optname_regex = re.compile('[^a-zA-Z0-9_-]')
  41. @permitted_kwargs({'value', 'yield'})
  42. def StringParser(name, description, kwargs):
  43. return coredata.UserStringOption(name,
  44. description,
  45. kwargs.get('value', ''),
  46. kwargs.get('choices', []),
  47. kwargs.get('yield', coredata.default_yielding))
  48. @permitted_kwargs({'value', 'yield'})
  49. def BooleanParser(name, description, kwargs):
  50. return coredata.UserBooleanOption(name, description,
  51. kwargs.get('value', True),
  52. kwargs.get('yield', coredata.default_yielding))
  53. @permitted_kwargs({'value', 'yield', 'choices'})
  54. def ComboParser(name, description, kwargs):
  55. if 'choices' not in kwargs:
  56. raise OptionException('Combo option missing "choices" keyword.')
  57. choices = kwargs['choices']
  58. if not isinstance(choices, list):
  59. raise OptionException('Combo choices must be an array.')
  60. for i in choices:
  61. if not isinstance(i, str):
  62. raise OptionException('Combo choice elements must be strings.')
  63. return coredata.UserComboOption(name,
  64. description,
  65. choices,
  66. kwargs.get('value', choices[0]),
  67. kwargs.get('yield', coredata.default_yielding),)
  68. @permitted_kwargs({'value', 'min', 'max', 'yield'})
  69. def IntegerParser(name, description, kwargs):
  70. if 'value' not in kwargs:
  71. raise OptionException('Integer option must contain value argument.')
  72. return coredata.UserIntegerOption(name,
  73. description,
  74. kwargs.get('min', None),
  75. kwargs.get('max', None),
  76. kwargs['value'],
  77. kwargs.get('yield', coredata.default_yielding))
  78. # FIXME: Cannot use FeatureNew while parsing options because we parse it before
  79. # reading options in project(). See func_project() in interpreter.py
  80. #@FeatureNew('array type option()', '0.44.0')
  81. @permitted_kwargs({'value', 'yield', 'choices'})
  82. def string_array_parser(name, description, kwargs):
  83. if 'choices' in kwargs:
  84. choices = kwargs['choices']
  85. if not isinstance(choices, list):
  86. raise OptionException('Array choices must be an array.')
  87. for i in choices:
  88. if not isinstance(i, str):
  89. raise OptionException('Array choice elements must be strings.')
  90. value = kwargs.get('value', choices)
  91. else:
  92. choices = None
  93. value = kwargs.get('value', [])
  94. if not isinstance(value, list):
  95. raise OptionException('Array choices must be passed as an array.')
  96. return coredata.UserArrayOption(name,
  97. description,
  98. value,
  99. choices=choices,
  100. yielding=kwargs.get('yield', coredata.default_yielding))
  101. @permitted_kwargs({'value', 'yield'})
  102. def FeatureParser(name, description, kwargs):
  103. return coredata.UserFeatureOption(name,
  104. description,
  105. kwargs.get('value', 'auto'),
  106. yielding=kwargs.get('yield', coredata.default_yielding))
  107. option_types = {'string': StringParser,
  108. 'boolean': BooleanParser,
  109. 'combo': ComboParser,
  110. 'integer': IntegerParser,
  111. 'array': string_array_parser,
  112. 'feature': FeatureParser,
  113. }
  114. class OptionInterpreter:
  115. def __init__(self, subproject):
  116. self.options = {}
  117. self.subproject = subproject
  118. def process(self, option_file):
  119. try:
  120. with open(option_file, 'r', encoding='utf8') as f:
  121. ast = mparser.Parser(f.read(), '').parse()
  122. except mesonlib.MesonException as me:
  123. me.file = option_file
  124. raise me
  125. if not isinstance(ast, mparser.CodeBlockNode):
  126. e = OptionException('Option file is malformed.')
  127. e.lineno = ast.lineno()
  128. raise e
  129. for cur in ast.lines:
  130. try:
  131. self.evaluate_statement(cur)
  132. except Exception as e:
  133. e.lineno = cur.lineno
  134. e.colno = cur.colno
  135. e.file = os.path.join('meson_options.txt')
  136. raise e
  137. def reduce_single(self, arg):
  138. if isinstance(arg, str):
  139. return arg
  140. elif isinstance(arg, (mparser.StringNode, mparser.BooleanNode,
  141. mparser.NumberNode)):
  142. return arg.value
  143. elif isinstance(arg, mparser.ArrayNode):
  144. return [self.reduce_single(curarg) for curarg in arg.args.arguments]
  145. else:
  146. raise OptionException('Arguments may only be string, int, bool, or array of those.')
  147. def reduce_arguments(self, args):
  148. assert(isinstance(args, mparser.ArgumentNode))
  149. if args.incorrect_order():
  150. raise OptionException('All keyword arguments must be after positional arguments.')
  151. reduced_pos = [self.reduce_single(arg) for arg in args.arguments]
  152. reduced_kw = {}
  153. for key in args.kwargs.keys():
  154. if not isinstance(key, str):
  155. raise OptionException('Keyword argument name is not a string.')
  156. a = args.kwargs[key]
  157. reduced_kw[key] = self.reduce_single(a)
  158. return reduced_pos, reduced_kw
  159. def evaluate_statement(self, node):
  160. if not isinstance(node, mparser.FunctionNode):
  161. raise OptionException('Option file may only contain option definitions')
  162. func_name = node.func_name
  163. if func_name != 'option':
  164. raise OptionException('Only calls to option() are allowed in option files.')
  165. (posargs, kwargs) = self.reduce_arguments(node.args)
  166. # FIXME: Cannot use FeatureNew while parsing options because we parse
  167. # it before reading options in project(). See func_project() in
  168. # interpreter.py
  169. #if 'yield' in kwargs:
  170. # FeatureNew('option yield', '0.45.0').use(self.subproject)
  171. if 'type' not in kwargs:
  172. raise OptionException('Option call missing mandatory "type" keyword argument')
  173. opt_type = kwargs.pop('type')
  174. if opt_type not in option_types:
  175. raise OptionException('Unknown type %s.' % opt_type)
  176. if len(posargs) != 1:
  177. raise OptionException('Option() must have one (and only one) positional argument')
  178. opt_name = posargs[0]
  179. if not isinstance(opt_name, str):
  180. raise OptionException('Positional argument must be a string.')
  181. if optname_regex.search(opt_name) is not None:
  182. raise OptionException('Option names can only contain letters, numbers or dashes.')
  183. if is_invalid_name(opt_name):
  184. raise OptionException('Option name %s is reserved.' % opt_name)
  185. if self.subproject != '':
  186. opt_name = self.subproject + ':' + opt_name
  187. opt = option_types[opt_type](opt_name, kwargs.pop('description', ''), kwargs)
  188. if opt.description == '':
  189. opt.description = opt_name
  190. self.options[opt_name] = opt