123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318 |
- #!/usr/bin/env python3
- # Copyright 2016 The Meson development team
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- # http://www.apache.org/licenses/LICENSE-2.0
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- # This class contains the basic functionality needed to run any interpreter
- # or an interpreter-based tool.
- # This tool is used to manipulate an existing Meson build definition.
- #
- # - add a file to a target
- # - remove files from a target
- # - move targets
- # - reindent?
- from .ast import IntrospectionInterpreter, build_target_functions, AstIDGenerator, AstIndentationGenerator, AstPrinter
- from mesonbuild.mesonlib import MesonException
- 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('-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):
- 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
|