aboutsummaryrefslogtreecommitdiff
path: root/mesonbuild/rewriter.py
diff options
context:
space:
mode:
Diffstat (limited to 'mesonbuild/rewriter.py')
-rw-r--r--mesonbuild/rewriter.py309
1 files changed, 284 insertions, 25 deletions
diff --git a/mesonbuild/rewriter.py b/mesonbuild/rewriter.py
index 37ed7ef..277835c 100644
--- a/mesonbuild/rewriter.py
+++ b/mesonbuild/rewriter.py
@@ -23,36 +23,295 @@
# - move targets
# - reindent?
-import mesonbuild.astinterpreter
+from .ast import IntrospectionInterpreter, build_target_functions, AstIDGenerator, AstIndentationGenerator, AstPrinter
from mesonbuild.mesonlib import MesonException
-from mesonbuild import mlog
-import sys, traceback
+from . import mlog, mparser, environment
+from functools import wraps
+from pprint import pprint
+import json, os
+
+class RewriterException(MesonException):
+ pass
def add_arguments(parser):
parser.add_argument('--sourcedir', default='.',
help='Path to source directory.')
- parser.add_argument('--target', default=None,
- help='Name of target to edit.')
- parser.add_argument('--filename', default=None,
- help='Name of source file to add or remove to target.')
- parser.add_argument('commands', nargs='+')
+ parser.add_argument('-p', '--print', action='store_true', default=False, dest='print',
+ help='Print the parsed AST.')
+ parser.add_argument('command', type=str)
+
+class RequiredKeys:
+ def __init__(self, keys):
+ self.keys = keys
+
+ def __call__(self, f):
+ @wraps(f)
+ def wrapped(*wrapped_args, **wrapped_kwargs):
+ assert(len(wrapped_args) >= 2)
+ cmd = wrapped_args[1]
+ for key, val in self.keys.items():
+ typ = val[0] # The type of the value
+ default = val[1] # The default value -- None is required
+ choices = val[2] # Valid choices -- None is for everything
+ if key not in cmd:
+ if default is not None:
+ cmd[key] = default
+ else:
+ raise RewriterException('Key "{}" is missing in object for {}'
+ .format(key, f.__name__))
+ if not isinstance(cmd[key], typ):
+ raise RewriterException('Invalid type of "{}". Required is {} but provided was {}'
+ .format(key, typ.__name__, type(cmd[key]).__name__))
+ if choices is not None:
+ assert(isinstance(choices, list))
+ if cmd[key] not in choices:
+ raise RewriterException('Invalid value of "{}": Possible values are {} but provided was "{}"'
+ .format(key, choices, cmd[key]))
+ return f(*wrapped_args, **wrapped_kwargs)
+
+ return wrapped
+
+rewriter_keys = {
+ 'target': {
+ 'target': (str, None, None),
+ 'operation': (str, None, ['src_add', 'src_rm', 'test']),
+ 'sources': (list, [], None),
+ 'debug': (bool, False, None)
+ }
+}
+
+class Rewriter:
+ def __init__(self, sourcedir: str, generator: str = 'ninja'):
+ self.sourcedir = sourcedir
+ self.interpreter = IntrospectionInterpreter(sourcedir, '', generator)
+ self.id_generator = AstIDGenerator()
+ self.modefied_nodes = []
+ self.functions = {
+ 'target': self.process_target,
+ }
+
+ def analyze_meson(self):
+ mlog.log('Analyzing meson file:', mlog.bold(os.path.join(self.sourcedir, environment.build_filename)))
+ self.interpreter.analyze()
+ mlog.log(' -- Project:', mlog.bold(self.interpreter.project_data['descriptive_name']))
+ mlog.log(' -- Version:', mlog.cyan(self.interpreter.project_data['version']))
+ self.interpreter.ast.accept(AstIndentationGenerator())
+ self.interpreter.ast.accept(self.id_generator)
+
+ def find_target(self, target: str):
+ for i in self.interpreter.targets:
+ if target == i['name'] or target == i['id']:
+ return i
+ return None
+
+ @RequiredKeys(rewriter_keys['target'])
+ def process_target(self, cmd):
+ mlog.log('Processing target', mlog.bold(cmd['target']), 'operation', mlog.cyan(cmd['operation']))
+ target = self.find_target(cmd['target'])
+ if target is None:
+ mlog.error('Unknown target "{}" --> skipping'.format(cmd['target']))
+ if cmd['debug']:
+ pprint(self.interpreter.targets)
+ return
+ if cmd['debug']:
+ pprint(target)
+
+ # Utility function to get a list of the sources from a node
+ def arg_list_from_node(n):
+ args = []
+ if isinstance(n, mparser.FunctionNode):
+ args = list(n.args.arguments)
+ if n.func_name in build_target_functions:
+ args.pop(0)
+ elif isinstance(n, mparser.ArrayNode):
+ args = n.args.arguments
+ elif isinstance(n, mparser.ArgumentNode):
+ args = n.arguments
+ return args
+
+ if cmd['operation'] == 'src_add':
+ node = None
+ if target['sources']:
+ node = target['sources'][0]
+ else:
+ node = target['node']
+ assert(node is not None)
+
+ # Generate the new String nodes
+ to_append = []
+ for i in cmd['sources']:
+ mlog.log(' -- Adding source', mlog.green(i), 'at',
+ mlog.yellow('{}:{}'.format(os.path.join(node.subdir, environment.build_filename), node.lineno)))
+ token = mparser.Token('string', node.subdir, 0, 0, 0, None, i)
+ to_append += [mparser.StringNode(token)]
+
+ # Append to the AST at the right place
+ if isinstance(node, mparser.FunctionNode):
+ node.args.arguments += to_append
+ elif isinstance(node, mparser.ArrayNode):
+ node.args.arguments += to_append
+ elif isinstance(node, mparser.ArgumentNode):
+ node.arguments += to_append
+
+ # Mark the node as modified
+ if node not in self.modefied_nodes:
+ self.modefied_nodes += [node]
+
+ elif cmd['operation'] == 'src_rm':
+ # Helper to find the exact string node and its parent
+ def find_node(src):
+ for i in target['sources']:
+ for j in arg_list_from_node(i):
+ if isinstance(j, mparser.StringNode):
+ if j.value == src:
+ return i, j
+ return None, None
+
+ for i in cmd['sources']:
+ # Try to find the node with the source string
+ root, string_node = find_node(i)
+ if root is None:
+ mlog.warning(' -- Unable to find source', mlog.green(i), 'in the target')
+ continue
+
+ # Remove the found string node from the argument list
+ arg_node = None
+ if isinstance(root, mparser.FunctionNode):
+ arg_node = root.args
+ if isinstance(root, mparser.ArrayNode):
+ arg_node = root.args
+ if isinstance(root, mparser.ArgumentNode):
+ arg_node = root
+ assert(arg_node is not None)
+ mlog.log(' -- Removing source', mlog.green(i), 'from',
+ mlog.yellow('{}:{}'.format(os.path.join(string_node.subdir, environment.build_filename), string_node.lineno)))
+ arg_node.arguments.remove(string_node)
+
+ # Mark the node as modified
+ if root not in self.modefied_nodes:
+ self.modefied_nodes += [root]
+
+ elif cmd['operation'] == 'test':
+ # List all sources in the target
+ src_list = []
+ for i in target['sources']:
+ for j in arg_list_from_node(i):
+ if isinstance(j, mparser.StringNode):
+ src_list += [j.value]
+ test_data = {
+ 'name': target['name'],
+ 'sources': src_list
+ }
+ mlog.log(' !! target {}={}'.format(target['id'], json.dumps(test_data)))
+
+ def process(self, cmd):
+ if 'type' not in cmd:
+ raise RewriterException('Command has no key "type"')
+ if cmd['type'] not in self.functions:
+ raise RewriterException('Unknown command "{}". Supported commands are: {}'
+ .format(cmd['type'], list(self.functions.keys())))
+ self.functions[cmd['type']](cmd)
+
+ def apply_changes(self):
+ assert(all(hasattr(x, 'lineno') and hasattr(x, 'colno') and hasattr(x, 'subdir') for x in self.modefied_nodes))
+ assert(all(isinstance(x, (mparser.ArrayNode, mparser.FunctionNode)) for x in self.modefied_nodes))
+ # Sort based on line and column in reversed order
+ work_nodes = list(sorted(self.modefied_nodes, key=lambda x: x.lineno * 1000 + x.colno, reverse=True))
+
+ # Generating the new replacement string
+ str_list = []
+ for i in work_nodes:
+ printer = AstPrinter()
+ i.accept(printer)
+ printer.post_process()
+ data = {
+ 'file': os.path.join(i.subdir, environment.build_filename),
+ 'str': printer.result.strip(),
+ 'node': i
+ }
+ str_list += [data]
+
+ # Load build files
+ files = {}
+ for i in str_list:
+ if i['file'] in files:
+ continue
+ fpath = os.path.realpath(os.path.join(self.sourcedir, i['file']))
+ fdata = ''
+ with open(fpath, 'r') as fp:
+ fdata = fp.read()
+
+ # Generate line offsets numbers
+ m_lines = fdata.splitlines(True)
+ offset = 0
+ line_offsets = []
+ for j in m_lines:
+ line_offsets += [offset]
+ offset += len(j)
+
+ files[i['file']] = {
+ 'path': fpath,
+ 'raw': fdata,
+ 'offsets': line_offsets
+ }
+
+ # Replace in source code
+ for i in str_list:
+ offsets = files[i['file']]['offsets']
+ raw = files[i['file']]['raw']
+ node = i['node']
+ line = node.lineno - 1
+ col = node.colno
+ start = offsets[line] + col
+ end = start
+ if isinstance(node, mparser.ArrayNode):
+ if raw[end] != '[':
+ mlog.warning('Internal error: expected "[" at {}:{} but got "{}"'.format(line, col, raw[end]))
+ continue
+ counter = 1
+ while counter > 0:
+ end += 1
+ if raw[end] == '[':
+ counter += 1
+ elif raw[end] == ']':
+ counter -= 1
+ end += 1
+ elif isinstance(node, mparser.FunctionNode):
+ while raw[end] != '(':
+ end += 1
+ end += 1
+ counter = 1
+ while counter > 0:
+ end += 1
+ if raw[end] == '(':
+ counter += 1
+ elif raw[end] == ')':
+ counter -= 1
+ end += 1
+ raw = files[i['file']]['raw'] = raw[:start] + i['str'] + raw[end:]
+
+ # Write the files back
+ for key, val in files.items():
+ mlog.log('Rewriting', mlog.yellow(key))
+ with open(val['path'], 'w') as fp:
+ fp.write(val['raw'])
def run(options):
- if options.target is None or options.filename is None:
- sys.exit("Must specify both target and filename.")
- print('This tool is highly experimental, use with care.')
- rewriter = mesonbuild.astinterpreter.RewriterInterpreter(options.sourcedir, '')
- try:
- if options.commands[0] == 'add':
- rewriter.add_source(options.target, options.filename)
- elif options.commands[0] == 'remove':
- rewriter.remove_source(options.target, options.filename)
- else:
- sys.exit('Unknown command: ' + options.commands[0])
- except Exception as e:
- if isinstance(e, MesonException):
- mlog.exception(e)
- else:
- traceback.print_exc()
- return 1
+ rewriter = Rewriter(options.sourcedir)
+ rewriter.analyze_meson()
+ if os.path.exists(options.command):
+ with open(options.command, 'r') as fp:
+ commands = json.load(fp)
+ else:
+ commands = json.loads(options.command)
+
+ if not isinstance(commands, list):
+ raise TypeError('Command is not a list')
+
+ for i in commands:
+ if not isinstance(i, object):
+ raise TypeError('Command is not an object')
+ rewriter.process(i)
+
+ rewriter.apply_changes()
return 0