rewriter.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. #!/usr/bin/env python3
  2. # Copyright 2016 The Meson development team
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. # Unless required by applicable law or agreed to in writing, software
  8. # distributed under the License is distributed on an "AS IS" BASIS,
  9. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. # See the License for the specific language governing permissions and
  11. # limitations under the License.
  12. # This class contains the basic functionality needed to run any interpreter
  13. # or an interpreter-based tool.
  14. # This tool is used to manipulate an existing Meson build definition.
  15. #
  16. # - add a file to a target
  17. # - remove files from a target
  18. # - move targets
  19. # - reindent?
  20. from .ast import IntrospectionInterpreter, build_target_functions, AstIDGenerator, AstIndentationGenerator, AstPrinter
  21. from mesonbuild.mesonlib import MesonException
  22. from . import mlog, mparser, environment
  23. from functools import wraps
  24. from pprint import pprint
  25. import json, os
  26. class RewriterException(MesonException):
  27. pass
  28. def add_arguments(parser):
  29. parser.add_argument('--sourcedir', default='.',
  30. help='Path to source directory.')
  31. parser.add_argument('-p', '--print', action='store_true', default=False, dest='print',
  32. help='Print the parsed AST.')
  33. parser.add_argument('command', type=str)
  34. class RequiredKeys:
  35. def __init__(self, keys):
  36. self.keys = keys
  37. def __call__(self, f):
  38. @wraps(f)
  39. def wrapped(*wrapped_args, **wrapped_kwargs):
  40. assert(len(wrapped_args) >= 2)
  41. cmd = wrapped_args[1]
  42. for key, val in self.keys.items():
  43. typ = val[0] # The type of the value
  44. default = val[1] # The default value -- None is required
  45. choices = val[2] # Valid choices -- None is for everything
  46. if key not in cmd:
  47. if default is not None:
  48. cmd[key] = default
  49. else:
  50. raise RewriterException('Key "{}" is missing in object for {}'
  51. .format(key, f.__name__))
  52. if not isinstance(cmd[key], typ):
  53. raise RewriterException('Invalid type of "{}". Required is {} but provided was {}'
  54. .format(key, typ.__name__, type(cmd[key]).__name__))
  55. if choices is not None:
  56. assert(isinstance(choices, list))
  57. if cmd[key] not in choices:
  58. raise RewriterException('Invalid value of "{}": Possible values are {} but provided was "{}"'
  59. .format(key, choices, cmd[key]))
  60. return f(*wrapped_args, **wrapped_kwargs)
  61. return wrapped
  62. rewriter_keys = {
  63. 'target': {
  64. 'target': (str, None, None),
  65. 'operation': (str, None, ['src_add', 'src_rm', 'test']),
  66. 'sources': (list, [], None),
  67. 'debug': (bool, False, None)
  68. }
  69. }
  70. class Rewriter:
  71. def __init__(self, sourcedir: str, generator: str = 'ninja'):
  72. self.sourcedir = sourcedir
  73. self.interpreter = IntrospectionInterpreter(sourcedir, '', generator)
  74. self.id_generator = AstIDGenerator()
  75. self.modefied_nodes = []
  76. self.functions = {
  77. 'target': self.process_target,
  78. }
  79. def analyze_meson(self):
  80. mlog.log('Analyzing meson file:', mlog.bold(os.path.join(self.sourcedir, environment.build_filename)))
  81. self.interpreter.analyze()
  82. mlog.log(' -- Project:', mlog.bold(self.interpreter.project_data['descriptive_name']))
  83. mlog.log(' -- Version:', mlog.cyan(self.interpreter.project_data['version']))
  84. self.interpreter.ast.accept(AstIndentationGenerator())
  85. self.interpreter.ast.accept(self.id_generator)
  86. def find_target(self, target: str):
  87. for i in self.interpreter.targets:
  88. if target == i['name'] or target == i['id']:
  89. return i
  90. return None
  91. @RequiredKeys(rewriter_keys['target'])
  92. def process_target(self, cmd):
  93. mlog.log('Processing target', mlog.bold(cmd['target']), 'operation', mlog.cyan(cmd['operation']))
  94. target = self.find_target(cmd['target'])
  95. if target is None:
  96. mlog.error('Unknown target "{}" --> skipping'.format(cmd['target']))
  97. if cmd['debug']:
  98. pprint(self.interpreter.targets)
  99. return
  100. if cmd['debug']:
  101. pprint(target)
  102. # Utility function to get a list of the sources from a node
  103. def arg_list_from_node(n):
  104. args = []
  105. if isinstance(n, mparser.FunctionNode):
  106. args = list(n.args.arguments)
  107. if n.func_name in build_target_functions:
  108. args.pop(0)
  109. elif isinstance(n, mparser.ArrayNode):
  110. args = n.args.arguments
  111. elif isinstance(n, mparser.ArgumentNode):
  112. args = n.arguments
  113. return args
  114. if cmd['operation'] == 'src_add':
  115. node = None
  116. if target['sources']:
  117. node = target['sources'][0]
  118. else:
  119. node = target['node']
  120. assert(node is not None)
  121. # Generate the new String nodes
  122. to_append = []
  123. for i in cmd['sources']:
  124. mlog.log(' -- Adding source', mlog.green(i), 'at',
  125. mlog.yellow('{}:{}'.format(os.path.join(node.subdir, environment.build_filename), node.lineno)))
  126. token = mparser.Token('string', node.subdir, 0, 0, 0, None, i)
  127. to_append += [mparser.StringNode(token)]
  128. # Append to the AST at the right place
  129. if isinstance(node, mparser.FunctionNode):
  130. node.args.arguments += to_append
  131. elif isinstance(node, mparser.ArrayNode):
  132. node.args.arguments += to_append
  133. elif isinstance(node, mparser.ArgumentNode):
  134. node.arguments += to_append
  135. # Mark the node as modified
  136. if node not in self.modefied_nodes:
  137. self.modefied_nodes += [node]
  138. elif cmd['operation'] == 'src_rm':
  139. # Helper to find the exact string node and its parent
  140. def find_node(src):
  141. for i in target['sources']:
  142. for j in arg_list_from_node(i):
  143. if isinstance(j, mparser.StringNode):
  144. if j.value == src:
  145. return i, j
  146. return None, None
  147. for i in cmd['sources']:
  148. # Try to find the node with the source string
  149. root, string_node = find_node(i)
  150. if root is None:
  151. mlog.warning(' -- Unable to find source', mlog.green(i), 'in the target')
  152. continue
  153. # Remove the found string node from the argument list
  154. arg_node = None
  155. if isinstance(root, mparser.FunctionNode):
  156. arg_node = root.args
  157. if isinstance(root, mparser.ArrayNode):
  158. arg_node = root.args
  159. if isinstance(root, mparser.ArgumentNode):
  160. arg_node = root
  161. assert(arg_node is not None)
  162. mlog.log(' -- Removing source', mlog.green(i), 'from',
  163. mlog.yellow('{}:{}'.format(os.path.join(string_node.subdir, environment.build_filename), string_node.lineno)))
  164. arg_node.arguments.remove(string_node)
  165. # Mark the node as modified
  166. if root not in self.modefied_nodes:
  167. self.modefied_nodes += [root]
  168. elif cmd['operation'] == 'test':
  169. # List all sources in the target
  170. src_list = []
  171. for i in target['sources']:
  172. for j in arg_list_from_node(i):
  173. if isinstance(j, mparser.StringNode):
  174. src_list += [j.value]
  175. test_data = {
  176. 'name': target['name'],
  177. 'sources': src_list
  178. }
  179. mlog.log(' !! target {}={}'.format(target['id'], json.dumps(test_data)))
  180. def process(self, cmd):
  181. if 'type' not in cmd:
  182. raise RewriterException('Command has no key "type"')
  183. if cmd['type'] not in self.functions:
  184. raise RewriterException('Unknown command "{}". Supported commands are: {}'
  185. .format(cmd['type'], list(self.functions.keys())))
  186. self.functions[cmd['type']](cmd)
  187. def apply_changes(self):
  188. assert(all(hasattr(x, 'lineno') and hasattr(x, 'colno') and hasattr(x, 'subdir') for x in self.modefied_nodes))
  189. assert(all(isinstance(x, (mparser.ArrayNode, mparser.FunctionNode)) for x in self.modefied_nodes))
  190. # Sort based on line and column in reversed order
  191. work_nodes = list(sorted(self.modefied_nodes, key=lambda x: x.lineno * 1000 + x.colno, reverse=True))
  192. # Generating the new replacement string
  193. str_list = []
  194. for i in work_nodes:
  195. printer = AstPrinter()
  196. i.accept(printer)
  197. printer.post_process()
  198. data = {
  199. 'file': os.path.join(i.subdir, environment.build_filename),
  200. 'str': printer.result.strip(),
  201. 'node': i
  202. }
  203. str_list += [data]
  204. # Load build files
  205. files = {}
  206. for i in str_list:
  207. if i['file'] in files:
  208. continue
  209. fpath = os.path.realpath(os.path.join(self.sourcedir, i['file']))
  210. fdata = ''
  211. with open(fpath, 'r') as fp:
  212. fdata = fp.read()
  213. # Generate line offsets numbers
  214. m_lines = fdata.splitlines(True)
  215. offset = 0
  216. line_offsets = []
  217. for j in m_lines:
  218. line_offsets += [offset]
  219. offset += len(j)
  220. files[i['file']] = {
  221. 'path': fpath,
  222. 'raw': fdata,
  223. 'offsets': line_offsets
  224. }
  225. # Replace in source code
  226. for i in str_list:
  227. offsets = files[i['file']]['offsets']
  228. raw = files[i['file']]['raw']
  229. node = i['node']
  230. line = node.lineno - 1
  231. col = node.colno
  232. start = offsets[line] + col
  233. end = start
  234. if isinstance(node, mparser.ArrayNode):
  235. if raw[end] != '[':
  236. mlog.warning('Internal error: expected "[" at {}:{} but got "{}"'.format(line, col, raw[end]))
  237. continue
  238. counter = 1
  239. while counter > 0:
  240. end += 1
  241. if raw[end] == '[':
  242. counter += 1
  243. elif raw[end] == ']':
  244. counter -= 1
  245. end += 1
  246. elif isinstance(node, mparser.FunctionNode):
  247. while raw[end] != '(':
  248. end += 1
  249. end += 1
  250. counter = 1
  251. while counter > 0:
  252. end += 1
  253. if raw[end] == '(':
  254. counter += 1
  255. elif raw[end] == ')':
  256. counter -= 1
  257. end += 1
  258. raw = files[i['file']]['raw'] = raw[:start] + i['str'] + raw[end:]
  259. # Write the files back
  260. for key, val in files.items():
  261. mlog.log('Rewriting', mlog.yellow(key))
  262. with open(val['path'], 'w') as fp:
  263. fp.write(val['raw'])
  264. def run(options):
  265. rewriter = Rewriter(options.sourcedir)
  266. rewriter.analyze_meson()
  267. if os.path.exists(options.command):
  268. with open(options.command, 'r') as fp:
  269. commands = json.load(fp)
  270. else:
  271. commands = json.loads(options.command)
  272. if not isinstance(commands, list):
  273. raise TypeError('Command is not a list')
  274. for i in commands:
  275. if not isinstance(i, object):
  276. raise TypeError('Command is not an object')
  277. rewriter.process(i)
  278. rewriter.apply_changes()
  279. return 0