optinterpreter.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  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. forbidden_option_names = coredata.get_builtin_options()
  17. forbidden_prefixes = {'c_',
  18. 'cpp_',
  19. 'd_',
  20. 'rust_',
  21. 'fortran_',
  22. 'objc_',
  23. 'objcpp_',
  24. 'vala_',
  25. 'csharp_',
  26. 'swift_',
  27. 'b_',
  28. 'backend_',
  29. }
  30. def is_invalid_name(name):
  31. if name in forbidden_option_names:
  32. return True
  33. pref = name.split('_')[0] + '_'
  34. if pref in forbidden_prefixes:
  35. return True
  36. return False
  37. class OptionException(mesonlib.MesonException):
  38. pass
  39. def permitted_kwargs(permitted):
  40. """Function that validates kwargs for options."""
  41. def _wraps(func):
  42. @functools.wraps(func)
  43. def _inner(name, description, kwargs):
  44. bad = [a for a in kwargs.keys() if a not in permitted]
  45. if bad:
  46. raise OptionException('Invalid kwargs for option "{}": "{}"'.format(
  47. name, ' '.join(bad)))
  48. return func(name, description, kwargs)
  49. return _inner
  50. return _wraps
  51. optname_regex = re.compile('[^a-zA-Z0-9_-]')
  52. @permitted_kwargs({'value'})
  53. def StringParser(name, description, kwargs):
  54. return coredata.UserStringOption(name, description,
  55. kwargs.get('value', ''), kwargs.get('choices', []))
  56. @permitted_kwargs({'value'})
  57. def BooleanParser(name, description, kwargs):
  58. return coredata.UserBooleanOption(name, description, kwargs.get('value', True))
  59. @permitted_kwargs({'value', 'choices'})
  60. def ComboParser(name, description, kwargs):
  61. if 'choices' not in kwargs:
  62. raise OptionException('Combo option missing "choices" keyword.')
  63. choices = kwargs['choices']
  64. if not isinstance(choices, list):
  65. raise OptionException('Combo choices must be an array.')
  66. for i in choices:
  67. if not isinstance(i, str):
  68. raise OptionException('Combo choice elements must be strings.')
  69. return coredata.UserComboOption(name, description, choices, kwargs.get('value', choices[0]))
  70. @permitted_kwargs({'value', 'choices'})
  71. def string_array_parser(name, description, kwargs):
  72. if 'choices' in kwargs:
  73. choices = kwargs['choices']
  74. if not isinstance(choices, list):
  75. raise OptionException('Array choices must be an array.')
  76. for i in choices:
  77. if not isinstance(i, str):
  78. raise OptionException('Array choice elements must be strings.')
  79. value = kwargs.get('value', choices)
  80. else:
  81. choices = None
  82. value = kwargs.get('value', [])
  83. if not isinstance(value, list):
  84. raise OptionException('Array choices must be passed as an array.')
  85. return coredata.UserArrayOption(name, description, value, choices=choices)
  86. option_types = {'string': StringParser,
  87. 'boolean': BooleanParser,
  88. 'combo': ComboParser,
  89. 'array': string_array_parser,
  90. }
  91. class OptionInterpreter:
  92. def __init__(self, subproject, command_line_options):
  93. self.options = {}
  94. self.subproject = subproject
  95. self.sbprefix = subproject + ':'
  96. self.cmd_line_options = {}
  97. for o in command_line_options:
  98. if self.subproject != '': # Strip the beginning.
  99. # Ignore options that aren't for this subproject
  100. if not o.startswith(self.sbprefix):
  101. continue
  102. try:
  103. (key, value) = o.split('=', 1)
  104. except ValueError:
  105. raise OptionException('Option {!r} must have a value separated by equals sign.'.format(o))
  106. # Ignore subproject options if not fetching subproject options
  107. if self.subproject == '' and ':' in key:
  108. continue
  109. self.cmd_line_options[key] = value
  110. def process(self, option_file):
  111. try:
  112. with open(option_file, 'r', encoding='utf8') as f:
  113. ast = mparser.Parser(f.read(), '').parse()
  114. except mesonlib.MesonException as me:
  115. me.file = option_file
  116. raise me
  117. if not isinstance(ast, mparser.CodeBlockNode):
  118. e = OptionException('Option file is malformed.')
  119. e.lineno = ast.lineno()
  120. raise e
  121. for cur in ast.lines:
  122. try:
  123. self.evaluate_statement(cur)
  124. except Exception as e:
  125. e.lineno = cur.lineno
  126. e.colno = cur.colno
  127. e.file = os.path.join('meson_options.txt')
  128. raise e
  129. def reduce_single(self, arg):
  130. if isinstance(arg, str):
  131. return arg
  132. elif isinstance(arg, (mparser.StringNode, mparser.BooleanNode,
  133. mparser.NumberNode)):
  134. return arg.value
  135. elif isinstance(arg, mparser.ArrayNode):
  136. return [self.reduce_single(curarg) for curarg in arg.args.arguments]
  137. else:
  138. raise OptionException('Arguments may only be string, int, bool, or array of those.')
  139. def reduce_arguments(self, args):
  140. assert(isinstance(args, mparser.ArgumentNode))
  141. if args.incorrect_order():
  142. raise OptionException('All keyword arguments must be after positional arguments.')
  143. reduced_pos = [self.reduce_single(arg) for arg in args.arguments]
  144. reduced_kw = {}
  145. for key in args.kwargs.keys():
  146. if not isinstance(key, str):
  147. raise OptionException('Keyword argument name is not a string.')
  148. a = args.kwargs[key]
  149. reduced_kw[key] = self.reduce_single(a)
  150. return reduced_pos, reduced_kw
  151. def evaluate_statement(self, node):
  152. if not isinstance(node, mparser.FunctionNode):
  153. raise OptionException('Option file may only contain option definitions')
  154. func_name = node.func_name
  155. if func_name != 'option':
  156. raise OptionException('Only calls to option() are allowed in option files.')
  157. (posargs, kwargs) = self.reduce_arguments(node.args)
  158. if 'type' not in kwargs:
  159. raise OptionException('Option call missing mandatory "type" keyword argument')
  160. opt_type = kwargs.pop('type')
  161. if opt_type not in option_types:
  162. raise OptionException('Unknown type %s.' % opt_type)
  163. if len(posargs) != 1:
  164. raise OptionException('Option() must have one (and only one) positional argument')
  165. opt_name = posargs[0]
  166. if not isinstance(opt_name, str):
  167. raise OptionException('Positional argument must be a string.')
  168. if optname_regex.search(opt_name) is not None:
  169. raise OptionException('Option names can only contain letters, numbers or dashes.')
  170. if is_invalid_name(opt_name):
  171. raise OptionException('Option name %s is reserved.' % opt_name)
  172. if self.subproject != '':
  173. opt_name = self.subproject + ':' + opt_name
  174. opt = option_types[opt_type](opt_name, kwargs.pop('description', ''), kwargs)
  175. if opt.description == '':
  176. opt.description = opt_name
  177. if opt_name in self.cmd_line_options:
  178. opt.set_value(self.cmd_line_options[opt_name])
  179. self.options[opt_name] = opt