| # SPDX-License-Identifier: Apache-2.0 | |
| # Copyright 2016 The Meson development team | |
| # 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 __future__ import annotations | |
| from .ast import IntrospectionInterpreter, BUILD_TARGET_FUNCTIONS, AstConditionLevel, AstIDGenerator, AstIndentationGenerator, AstPrinter | |
| from .ast.interpreter import IntrospectionBuildTarget, IntrospectionDependency, _symbol | |
| from .interpreterbase import UnknownValue | |
| from .interpreterbase.helpers import flatten | |
| from mesonbuild.mesonlib import MesonException, pathname_sort_key, relpath, setup_vsenv | |
| from . import mlog, environment | |
| from functools import wraps | |
| from .mparser import Token, ArrayNode, ArgumentNode, ArithmeticNode, AssignmentNode, BaseNode, StringNode, BooleanNode, DictNode, ElementaryNode, IdNode, FunctionNode, PlusAssignmentNode | |
| from .mintro import IntrospectionEncoder | |
| import json, os, re, sys, codecs | |
| import typing as T | |
| from pathlib import Path | |
| if T.TYPE_CHECKING: | |
| import argparse | |
| from argparse import ArgumentParser, _FormatterClass | |
| from typing_extensions import TypeAlias | |
| from .mlog import AnsiDecorator | |
| RewriterKeysT: TypeAlias = dict[str, tuple[type, object | None, list[str] | None]] | |
| RewriterFuncT: TypeAlias = T.Callable[['Rewriter', dict[str, object]], None] | |
| class RewriterException(MesonException): | |
| pass | |
| # Note: when adding arguments, please also add them to the completion | |
| # scripts in $MESONSRC/data/shell-completions/ | |
| def add_arguments(parser: ArgumentParser, formatter: _FormatterClass) -> None: | |
| parser.add_argument('-s', '--sourcedir', type=str, default='.', metavar='SRCDIR', help='Path to source directory.') | |
| parser.add_argument('-V', '--verbose', action='store_true', default=False, help='Enable verbose output') | |
| parser.add_argument('-S', '--skip-errors', dest='skip', action='store_true', default=False, help='Skip errors instead of aborting') | |
| subparsers = parser.add_subparsers(dest='type', title='Rewriter commands', description='Rewrite command to execute') | |
| # Target | |
| tgt_parser = subparsers.add_parser('target', aliases=['tgt'], help='Modify a target', formatter_class=formatter) | |
| tgt_parser.add_argument('-s', '--subdir', default='', dest='subdir', help='Subdirectory of the new target (only for the "add_target" action)') | |
| tgt_parser.add_argument('--type', dest='tgt_type', choices=rewriter_keys['target']['target_type'][2], default='executable', | |
| help='Type of the target to add (only for the "add_target" action)') | |
| tgt_parser.add_argument('target', help='Name or ID of the target') | |
| tgt_parser.add_argument('operation', choices=['add', 'rm', 'add_target', 'rm_target', 'add_extra_files', 'rm_extra_files', 'info'], | |
| help='Action to execute') | |
| tgt_parser.add_argument('sources', nargs='*', help='Sources to add/remove') | |
| # KWARGS | |
| kw_parser = subparsers.add_parser('kwargs', help='Modify keyword arguments', formatter_class=formatter) | |
| kw_parser.add_argument('operation', choices=rewriter_keys['kwargs']['operation'][2], | |
| help='Action to execute') | |
| kw_parser.add_argument('function', choices=list(rewriter_func_kwargs.keys()), | |
| help='Function type to modify') | |
| kw_parser.add_argument('id', help='ID of the function to modify (must be "/" for "project")') | |
| kw_parser.add_argument('kwargs', nargs='*', help='<keyword> <value> pairs, or list of <keyword> for "delete"') | |
| # Default options | |
| def_parser = subparsers.add_parser('default-options', aliases=['def'], help='Modify the project default options', formatter_class=formatter) | |
| def_parser.add_argument('operation', choices=rewriter_keys['default_options']['operation'][2], | |
| help='Action to execute') | |
| def_parser.add_argument('options', nargs='*', help='<key> <value> pairs for "set"; list of <key> for "delete"') | |
| # JSON file/command | |
| cmd_parser = subparsers.add_parser('command', aliases=['cmd'], help='Execute a JSON array of commands', formatter_class=formatter) | |
| cmd_parser.add_argument('json', help='JSON string or file to execute') | |
| class RequiredKeys: | |
| def __init__(self, keys: RewriterKeysT): | |
| self.keys = keys | |
| def __call__(self, f: RewriterFuncT) -> RewriterFuncT: | |
| @wraps(f) | |
| def wrapped(rewriter: 'Rewriter', cmd: dict[str, object]) -> None: | |
| for key, (typ, default, choices) in self.keys.items(): | |
| 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: | |
| if cmd[key] not in choices: | |
| raise RewriterException('Invalid value of "{}": Possible values are {} but provided was "{}"' | |
| .format(key, choices, cmd[key])) | |
| return f(rewriter, cmd) | |
| return wrapped | |
| class MTypeBase: | |
| node: BaseNode | |
| def __init__(self, node: T.Optional[BaseNode] = None): | |
| if node is None: | |
| self.node = self.new_node() | |
| else: | |
| self.node = node | |
| self.node_type = None | |
| for i in self.supported_nodes(): | |
| if isinstance(self.node, i): | |
| self.node_type = i | |
| @classmethod | |
| def new_node(cls, value: T.Any = None) -> BaseNode: | |
| # Overwrite in derived class | |
| raise RewriterException('Internal error: new_node of MTypeBase was called') | |
| @classmethod | |
| def supported_nodes(cls) -> T.List[type]: | |
| # Overwrite in derived class | |
| return [] | |
| def can_modify(self) -> bool: | |
| return self.node_type is not None | |
| def get_node(self) -> BaseNode: | |
| return self.node | |
| def add_value(self, value: T.Any) -> None: | |
| # Overwrite in derived class | |
| mlog.warning('Cannot add a value of type', mlog.bold(type(self).__name__), '--> skipping') | |
| def remove_value(self, value: T.Any) -> None: | |
| # Overwrite in derived class | |
| mlog.warning('Cannot remove a value of type', mlog.bold(type(self).__name__), '--> skipping') | |
| def remove_regex(self, value: T.Any) -> None: | |
| # Overwrite in derived class | |
| mlog.warning('Cannot remove a regex in type', mlog.bold(type(self).__name__), '--> skipping') | |
| class MTypeStr(MTypeBase): | |
| def __init__(self, node: T.Optional[BaseNode] = None): | |
| super().__init__(node) | |
| @classmethod | |
| def new_node(cls, value: T.Optional[str] = None) -> BaseNode: | |
| if value is None: | |
| value = '' | |
| return StringNode(Token('string', '', 0, 0, 0, None, str(value))) | |
| @classmethod | |
| def supported_nodes(cls) -> T.List[type]: | |
| return [StringNode] | |
| class MTypeBool(MTypeBase): | |
| def __init__(self, node: T.Optional[BaseNode] = None): | |
| super().__init__(node) | |
| @classmethod | |
| def new_node(cls, value: T.Optional[str] = None) -> BaseNode: | |
| return BooleanNode(Token('', '', 0, 0, 0, None, bool(value))) | |
| @classmethod | |
| def supported_nodes(cls) -> T.List[type]: | |
| return [BooleanNode] | |
| class MTypeID(MTypeBase): | |
| def __init__(self, node: T.Optional[BaseNode] = None): | |
| super().__init__(node) | |
| @classmethod | |
| def new_node(cls, value: T.Optional[str] = None) -> BaseNode: | |
| if value is None: | |
| value = '' | |
| return IdNode(Token('', '', 0, 0, 0, None, str(value))) | |
| @classmethod | |
| def supported_nodes(cls) -> T.List[type]: | |
| return [IdNode] | |
| class MTypeList(MTypeBase): | |
| node: ArrayNode | |
| def __init__(self, node: T.Optional[BaseNode] = None): | |
| super().__init__(node) | |
| @classmethod | |
| def new_node(cls, value: T.Optional[T.List[T.Any]] = None) -> ArrayNode: | |
| if value is None: | |
| value = [] | |
| elif not isinstance(value, list): | |
| return cls._new_element_node(value) | |
| args = ArgumentNode(Token('', '', 0, 0, 0, None, '')) | |
| args.arguments = [cls._new_element_node(i) for i in value] | |
| return ArrayNode(_symbol('['), args, _symbol(']')) | |
| @classmethod | |
| def _new_element_node(cls, value: T.Any) -> BaseNode: | |
| # Overwrite in derived class | |
| raise RewriterException('Internal error: _new_element_node of MTypeList was called') | |
| def _ensure_array_node(self) -> None: | |
| if not isinstance(self.node, ArrayNode): | |
| tmp = self.node | |
| self.node = self.new_node() | |
| self.node.args.arguments = [tmp] | |
| @staticmethod | |
| def _check_is_equal(node: BaseNode, value: str) -> bool: | |
| # Overwrite in derived class | |
| return False | |
| @staticmethod | |
| def _check_regex_matches(node: BaseNode, regex: str) -> bool: | |
| # Overwrite in derived class | |
| return False | |
| def get_node(self) -> BaseNode: | |
| if isinstance(self.node, ArrayNode): | |
| if len(self.node.args.arguments) == 1: | |
| return self.node.args.arguments[0] | |
| return self.node | |
| @classmethod | |
| def supported_element_nodes(cls) -> T.List[T.Type]: | |
| # Overwrite in derived class | |
| return [] | |
| @classmethod | |
| def supported_nodes(cls) -> T.List[T.Type]: | |
| return [ArrayNode] + cls.supported_element_nodes() | |
| def add_value(self, value: T.Any) -> None: | |
| if not isinstance(value, list): | |
| value = [value] | |
| self._ensure_array_node() | |
| for i in value: | |
| assert hasattr(self.node, 'args') # For mypy | |
| assert isinstance(self.node.args, ArgumentNode) # For mypy | |
| self.node.args.arguments += [self._new_element_node(i)] | |
| def _remove_helper(self, value: T.Any, equal_func: T.Callable[[T.Any, T.Any], bool]) -> None: | |
| def check_remove_node(node: BaseNode) -> bool: | |
| for j in value: | |
| if equal_func(i, j): | |
| return True | |
| return False | |
| if not isinstance(value, list): | |
| value = [value] | |
| self._ensure_array_node() | |
| assert hasattr(self.node, 'args') # For mypy | |
| assert isinstance(self.node.args, ArgumentNode) # For mypy | |
| removed_list = [] | |
| for i in self.node.args.arguments: | |
| if not check_remove_node(i): | |
| removed_list += [i] | |
| self.node.args.arguments = removed_list | |
| def remove_value(self, value: T.Any) -> None: | |
| self._remove_helper(value, self._check_is_equal) | |
| def remove_regex(self, regex: str) -> None: | |
| self._remove_helper(regex, self._check_regex_matches) | |
| class MTypeStrList(MTypeList): | |
| def __init__(self, node: T.Optional[BaseNode] = None): | |
| super().__init__(node) | |
| @classmethod | |
| def _new_element_node(cls, value: str) -> StringNode: | |
| return StringNode(Token('string', '', 0, 0, 0, None, str(value))) | |
| @staticmethod | |
| def _check_is_equal(node: BaseNode, value: str) -> bool: | |
| if isinstance(node, StringNode): | |
| return bool(node.value == value) | |
| return False | |
| @staticmethod | |
| def _check_regex_matches(node: BaseNode, regex: str) -> bool: | |
| if isinstance(node, StringNode): | |
| return re.match(regex, node.value) is not None | |
| return False | |
| @classmethod | |
| def supported_element_nodes(cls) -> T.List[T.Type]: | |
| return [StringNode] | |
| class MTypeIDList(MTypeList): | |
| def __init__(self, node: T.Optional[BaseNode] = None): | |
| super().__init__(node) | |
| @classmethod | |
| def _new_element_node(cls, value: str) -> IdNode: | |
| return IdNode(Token('', '', 0, 0, 0, None, str(value))) | |
| @staticmethod | |
| def _check_is_equal(node: BaseNode, value: str) -> bool: | |
| if isinstance(node, IdNode): | |
| return bool(node.value == value) | |
| return False | |
| @staticmethod | |
| def _check_regex_matches(node: BaseNode, regex: str) -> bool: | |
| if isinstance(node, StringNode): | |
| return re.match(regex, node.value) is not None | |
| return False | |
| @classmethod | |
| def supported_element_nodes(cls) -> T.List[T.Type]: | |
| return [IdNode] | |
| rewriter_keys: T.Dict[str, RewriterKeysT] = { | |
| 'default_options': { | |
| 'operation': (str, None, ['set', 'delete']), | |
| 'options': (dict, {}, None) | |
| }, | |
| 'kwargs': { | |
| 'function': (str, None, None), | |
| 'id': (str, None, None), | |
| 'operation': (str, None, ['set', 'delete', 'add', 'remove', 'remove_regex', 'info']), | |
| 'kwargs': (dict, {}, None) | |
| }, | |
| 'target': { | |
| 'target': (str, None, None), | |
| 'operation': (str, None, ['src_add', 'src_rm', 'target_rm', 'target_add', 'extra_files_add', 'extra_files_rm', 'info']), | |
| 'sources': (list, [], None), | |
| 'subdir': (str, '', None), | |
| 'target_type': (str, 'executable', ['both_libraries', 'executable', 'jar', 'library', 'shared_library', 'shared_module', 'static_library']), | |
| } | |
| } | |
| rewriter_func_kwargs = { | |
| 'dependency': { | |
| 'language': MTypeStr, | |
| 'method': MTypeStr, | |
| 'native': MTypeBool, | |
| 'not_found_message': MTypeStr, | |
| 'required': MTypeBool, | |
| 'static': MTypeBool, | |
| 'version': MTypeStrList, | |
| 'modules': MTypeStrList | |
| }, | |
| 'target': { | |
| 'build_by_default': MTypeBool, | |
| 'build_rpath': MTypeStr, | |
| 'dependencies': MTypeIDList, | |
| 'gui_app': MTypeBool, | |
| 'link_with': MTypeIDList, | |
| 'export_dynamic': MTypeBool, | |
| 'implib': MTypeBool, | |
| 'install': MTypeBool, | |
| 'install_dir': MTypeStr, | |
| 'install_rpath': MTypeStr, | |
| 'pie': MTypeBool | |
| }, | |
| 'project': { | |
| 'default_options': MTypeStrList, | |
| 'meson_version': MTypeStr, | |
| 'license': MTypeStrList, | |
| 'license_files': MTypeStrList, | |
| 'subproject_dir': MTypeStr, | |
| 'version': MTypeStr | |
| } | |
| } | |
| class Rewriter: | |
| info_dump: T.Optional[T.Dict[str, T.Dict[str, T.Any]]] | |
| def __init__(self, sourcedir: str, generator: str = 'ninja', skip_errors: bool = False): | |
| self.sourcedir = sourcedir | |
| self.interpreter = IntrospectionInterpreter(sourcedir, '', generator, visitors = [AstIDGenerator(), AstIndentationGenerator(), AstConditionLevel()]) | |
| self.skip_errors = skip_errors | |
| self.modified_nodes: T.List[BaseNode] = [] | |
| self.to_remove_nodes: T.List[BaseNode] = [] | |
| self.to_add_nodes: T.List[BaseNode] = [] | |
| self.functions = { | |
| 'default_options': self.process_default_options, | |
| 'kwargs': self.process_kwargs, | |
| 'target': self.process_target, | |
| } | |
| self.info_dump = None | |
| def analyze_meson(self) -> None: | |
| 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'])) | |
| def add_info(self, cmd_type: str, cmd_id: str, data: dict) -> None: | |
| if self.info_dump is None: | |
| self.info_dump = {} | |
| if cmd_type not in self.info_dump: | |
| self.info_dump[cmd_type] = {} | |
| self.info_dump[cmd_type][cmd_id] = data | |
| def print_info(self) -> None: | |
| if self.info_dump is None: | |
| return | |
| sys.stdout.write(json.dumps(self.info_dump, indent=2, cls=IntrospectionEncoder)) | |
| def on_error(self) -> T.Tuple[AnsiDecorator, AnsiDecorator]: | |
| if self.skip_errors: | |
| return mlog.cyan('-->'), mlog.yellow('skipping') | |
| return mlog.cyan('-->'), mlog.red('aborting') | |
| def handle_error(self) -> None: | |
| if self.skip_errors: | |
| return None | |
| raise MesonException('Rewriting the meson.build failed') | |
| def all_assignments(self, varname: str) -> T.List[BaseNode]: | |
| assigned_values = [] | |
| for ass in self.interpreter.all_assignment_nodes[varname]: | |
| if isinstance(ass, PlusAssignmentNode): | |
| continue | |
| assert isinstance(ass, AssignmentNode) | |
| assigned_values.append(ass.value) | |
| return assigned_values | |
| def find_target(self, target: str) -> T.Optional[IntrospectionBuildTarget]: | |
| for i in self.interpreter.targets: | |
| if target == i.id: | |
| return i | |
| potential_tgts = [] | |
| for i in self.interpreter.targets: | |
| if target == i.name: | |
| potential_tgts.append(i) | |
| if not potential_tgts: | |
| potenial_tgts_1 = self.all_assignments(target) | |
| potenial_tgts_1 = [self.interpreter.node_to_runtime_value(el) for el in potenial_tgts_1] | |
| potential_tgts = [el for el in potenial_tgts_1 if isinstance(el, IntrospectionBuildTarget)] | |
| if not potential_tgts: | |
| return None | |
| elif len(potential_tgts) == 1: | |
| return potential_tgts[0] | |
| else: | |
| mlog.error('There are multiple targets matching', mlog.bold(target)) | |
| for i in potential_tgts: | |
| mlog.error(' -- Target name', mlog.bold(i.name), 'with ID', mlog.bold(i.id)) | |
| mlog.error('Please try again with the unique ID of the target', *self.on_error()) | |
| self.handle_error() | |
| return None | |
| def find_dependency(self, dependency: str) -> T.Optional[IntrospectionDependency]: | |
| potential_deps = [] | |
| for i in self.interpreter.dependencies: | |
| if i.name == dependency: | |
| potential_deps.append(i) | |
| checking_varnames = len(potential_deps) == 0 | |
| if checking_varnames: | |
| potential_deps1 = self.all_assignments(dependency) | |
| potential_deps = [self.interpreter.node_to_runtime_value(el) for el in potential_deps1 if isinstance(el, FunctionNode) and el.func_name.value == 'dependency'] | |
| if not potential_deps: | |
| return None | |
| elif len(potential_deps) == 1: | |
| return potential_deps[0] | |
| else: | |
| mlog.error('There are multiple dependencies matching', mlog.bold(dependency)) | |
| for i in potential_deps: | |
| mlog.error(' -- Dependency name', i) | |
| if checking_varnames: | |
| mlog.error('Please try again with the name of the dependency', *self.on_error()) | |
| self.handle_error() | |
| return None | |
| @RequiredKeys(rewriter_keys['default_options']) | |
| def process_default_options(self, cmd: T.Dict[str, T.Any]) -> None: | |
| # First, remove the old values | |
| kwargs_cmd: T.Dict[str, T.Any] = { | |
| 'function': 'project', | |
| 'id': "/", | |
| 'operation': 'remove_regex', | |
| 'kwargs': { | |
| 'default_options': [f'{x}=.*' for x in cmd['options'].keys()] | |
| } | |
| } | |
| self.process_kwargs(kwargs_cmd) | |
| # Then add the new values | |
| if cmd['operation'] != 'set': | |
| return | |
| kwargs_cmd['operation'] = 'add' | |
| kwargs_cmd['kwargs']['default_options'] = [] | |
| cdata = self.interpreter.coredata | |
| options = { | |
| **{str(k): v for k, v in cdata.optstore.items()}, | |
| } | |
| for key, val in sorted(cmd['options'].items()): | |
| if key not in options: | |
| mlog.error('Unknown options', mlog.bold(key), *self.on_error()) | |
| self.handle_error() | |
| continue | |
| try: | |
| val = options[key].validate_value(val) | |
| except MesonException as e: | |
| mlog.error('Unable to set', mlog.bold(key), mlog.red(str(e)), *self.on_error()) | |
| self.handle_error() | |
| continue | |
| kwargs_cmd['kwargs']['default_options'] += [f'{key}={val}'] | |
| self.process_kwargs(kwargs_cmd) | |
| @RequiredKeys(rewriter_keys['kwargs']) | |
| def process_kwargs(self, cmd: T.Dict[str, T.Any]) -> None: | |
| mlog.log('Processing function type', mlog.bold(cmd['function']), 'with id', mlog.cyan("'" + cmd['id'] + "'")) | |
| if cmd['function'] not in rewriter_func_kwargs: | |
| mlog.error('Unknown function type', cmd['function'], *self.on_error()) | |
| return self.handle_error() | |
| kwargs_def = rewriter_func_kwargs[cmd['function']] | |
| # Find the function node to modify | |
| node = None | |
| arg_node = None | |
| if cmd['function'] == 'project': | |
| # msys bash may expand '/' to a path. It will mangle '//' to '/' | |
| # but in order to keep usage shell-agnostic, also allow `//` as | |
| # the function ID such that it will work in both msys bash and | |
| # other shells. | |
| if {'/', '//'}.isdisjoint({cmd['id']}): | |
| mlog.error('The ID for the function type project must be "/" or "//" not "' + cmd['id'] + '"', *self.on_error()) | |
| return self.handle_error() | |
| node = self.interpreter.project_node | |
| arg_node = node.args | |
| elif cmd['function'] == 'target': | |
| tmp_tgt = self.find_target(cmd['id']) | |
| if not tmp_tgt: | |
| mlog.error('Unable to find the target', mlog.bold(cmd['id']), *self.on_error()) | |
| return self.handle_error() | |
| node = tmp_tgt.node | |
| arg_node = node.args | |
| elif cmd['function'] == 'dependency': | |
| tmp_dep = self.find_dependency(cmd['id']) | |
| if not tmp_dep: | |
| mlog.error('Unable to find the dependency', mlog.bold(cmd['id']), *self.on_error()) | |
| return self.handle_error() | |
| node = tmp_dep.node | |
| arg_node = node.args | |
| if not node: | |
| mlog.error('Unable to find the function node', *self.on_error()) | |
| return self.handle_error() | |
| assert isinstance(node, FunctionNode) | |
| assert isinstance(arg_node, ArgumentNode) | |
| # Transform the key nodes to plain strings | |
| kwargs = {T.cast(IdNode, k).value: v for k, v in arg_node.kwargs.items()} | |
| # Print kwargs info | |
| if cmd['operation'] == 'info': | |
| info_data: T.Dict[str, T.Any] = {} | |
| for key, val in sorted(kwargs.items()): | |
| info_data[key] = None | |
| if isinstance(val, ElementaryNode): | |
| info_data[key] = val.value | |
| elif isinstance(val, ArrayNode): | |
| data_list = [] | |
| for i in val.args.arguments: | |
| element = None | |
| if isinstance(i, ElementaryNode): | |
| element = i.value | |
| data_list += [element] | |
| info_data[key] = data_list | |
| elif isinstance(val, DictNode): | |
| data_dict = {} | |
| for k, v in val.args.kwargs.items(): | |
| if not isinstance(k, StringNode): | |
| continue | |
| value = None | |
| if isinstance(v, ElementaryNode): | |
| value = v.value | |
| data_dict[k.value] = value | |
| info_data[key] = data_dict | |
| self.add_info('kwargs', '{}#{}'.format(cmd['function'], cmd['id']), info_data) | |
| return # Nothing else to do | |
| # Modify the kwargs | |
| num_changed = 0 | |
| for key, val in sorted(cmd['kwargs'].items()): | |
| if key not in kwargs_def: | |
| mlog.error('Cannot modify unknown kwarg', mlog.bold(key), *self.on_error()) | |
| self.handle_error() | |
| continue | |
| if cmd['operation'] == 'delete': | |
| # Remove the key from the kwargs | |
| if key not in kwargs: | |
| mlog.log(' -- Key', mlog.bold(key), 'is already deleted') | |
| continue | |
| mlog.log(' -- Deleting', mlog.bold(key), 'from the kwargs') | |
| del kwargs[key] | |
| elif cmd['operation'] == 'set': | |
| # Replace the key from the kwargs | |
| mlog.log(' -- Setting', mlog.bold(key), 'to', mlog.yellow(str(val))) | |
| kwargs[key] = kwargs_def[key].new_node(val) | |
| else: | |
| # Modify the value from the kwargs | |
| if key not in kwargs: | |
| kwargs[key] = None | |
| modifier = kwargs_def[key](kwargs[key]) | |
| if not modifier.can_modify(): | |
| mlog.log(' -- Skipping', mlog.bold(key), 'because it is too complex to modify') | |
| continue | |
| # Apply the operation | |
| val_str = str(val) | |
| if cmd['operation'] == 'add': | |
| mlog.log(' -- Adding', mlog.yellow(val_str), 'to', mlog.bold(key)) | |
| modifier.add_value(val) | |
| elif cmd['operation'] == 'remove': | |
| mlog.log(' -- Removing', mlog.yellow(val_str), 'from', mlog.bold(key)) | |
| modifier.remove_value(val) | |
| elif cmd['operation'] == 'remove_regex': | |
| mlog.log(' -- Removing all values matching', mlog.yellow(val_str), 'from', mlog.bold(key)) | |
| modifier.remove_regex(val) | |
| # Write back the result | |
| kwargs[key] = modifier.get_node() | |
| num_changed += 1 | |
| # Convert the keys back to IdNode's | |
| arg_node.kwargs = {IdNode(Token('', '', 0, 0, 0, None, k)): v for k, v in kwargs.items()} | |
| for k, v in arg_node.kwargs.items(): | |
| k.level = v.level | |
| if num_changed > 0 and node not in self.modified_nodes: | |
| self.modified_nodes += [node] | |
| def find_assignment_node(self, node: BaseNode) -> T.Optional[AssignmentNode]: | |
| for k, v in self.interpreter.all_assignment_nodes.items(): | |
| for ass in v: | |
| if ass.value == node: | |
| return ass | |
| return None | |
| def affects_no_other_targets(self, candidate: BaseNode) -> bool: | |
| affected = self.interpreter.dataflow_dag.reachable({candidate}, False) | |
| affected_targets = [x for x in affected if isinstance(x, FunctionNode) and x.func_name.value in BUILD_TARGET_FUNCTIONS] | |
| return len(affected_targets) == 1 | |
| def get_relto(self, target_node: BaseNode, node: BaseNode) -> Path: | |
| cwd = Path(os.getcwd()) | |
| all_paths = self.interpreter.dataflow_dag.find_all_paths(node, target_node) | |
| # len(all_paths) == 0 would imply that data does not flow from node to | |
| # target_node. This would imply that adding sources to node would not | |
| # add the source to the target. | |
| assert all_paths | |
| if len(all_paths) > 1: | |
| return None | |
| return (cwd / next(x for x in all_paths[0] if isinstance(x, FunctionNode)).filename).parent | |
| def add_src_or_extra(self, op: str, target: IntrospectionBuildTarget, newfiles: T.List[str], to_sort_nodes: T.List[T.Union[FunctionNode, ArrayNode]]) -> None: | |
| assert op in {'src_add', 'extra_files_add'} | |
| if op == 'src_add': | |
| old: T.Set[T.Union[BaseNode, UnknownValue]] = set(target.source_nodes) | |
| elif op == 'extra_files_add': | |
| if target.extra_files is None: | |
| old = set() | |
| else: | |
| old = {target.extra_files} | |
| tgt_function: FunctionNode = target.node | |
| cwd = Path(os.getcwd()) | |
| target_dir_abs = cwd / os.path.dirname(target.node.filename) | |
| source_root_abs = cwd / self.interpreter.source_root | |
| candidates1 = self.interpreter.dataflow_dag.reachable(old, True) | |
| # A node is a member of the set `candidates1` exactly if data from this node | |
| # flow into one of the `dest` nodes. We assume that this implies that if we | |
| # add `foo.c` to this node, then 'foo.c' will be added to one of these | |
| # nodes. This assumption is not always true: | |
| # ar = ['a.c', 'b.c'] | |
| # srcs = ar[1] | |
| # executable('name', srcs) | |
| # Data flows from `ar` to `srcs`, but if we add 'foo.c': | |
| # ar = ['a.c', 'b.c', 'foo.c'] | |
| # srcs = ar[1] | |
| # executable('name', srcs) | |
| # this does not add 'foo.c' to `srcs`. This is a known bug/limitation of | |
| # the meson rewriter that could be fixed by replacing `reachable` with a | |
| # more advanced analysis. But this is a lot of work and I think e.g. | |
| # `srcs = ar[1]` is rare in real-world projects, so I will just leave | |
| # this for now. | |
| candidates2 = {x for x in candidates1 if isinstance(x, (FunctionNode, ArrayNode))} | |
| # If we have this meson.build file: | |
| # shared = ['shared.c'] | |
| # executable('foo', shared + ['foo.c']) | |
| # executable('bar', shared + ['bar.c']) | |
| # and we are tasked with adding 'new.c' to 'foo', we should do e.g this: | |
| # shared = ['shared.c'] | |
| # executable('foo', shared + ['foo.c', 'new.c']) | |
| # executable('bar', shared + ['bar.c']) | |
| # but never this: | |
| # shared = ['shared.c', 'new.c'] | |
| # executable('foo', shared + ['foo.c']) | |
| # executable('bar', shared + ['bar.c']) | |
| # We do this by removing the `['shared.c']`-node from `candidates2`. | |
| candidates2 = {x for x in candidates2 if self.affects_no_other_targets(x)} | |
| def path_contains_unknowns(candidate: BaseNode) -> bool: | |
| all_paths = self.interpreter.dataflow_dag.find_all_paths(candidate, target.node) | |
| for path in all_paths: | |
| for el in path: | |
| if isinstance(el, UnknownValue): | |
| return True | |
| return False | |
| candidates2 = {x for x in candidates2 if not path_contains_unknowns(x)} | |
| candidates2 = {x for x in candidates2 if self.get_relto(target.node, x) is not None} | |
| chosen: T.Union[FunctionNode, ArrayNode] = None | |
| new_kwarg_flag = False | |
| if len(candidates2) > 0: | |
| # So that files(['a', 'b']) gets modified to files(['a', 'b', 'c']) instead of files(['a', 'b'], 'c') | |
| if len({x for x in candidates2 if isinstance(x, ArrayNode)}) > 0: | |
| candidates2 = {x for x in candidates2 if isinstance(x, ArrayNode)} | |
| # We choose one more or less arbitrary candidate | |
| chosen = min(candidates2, key=lambda x: (x.lineno, x.colno)) | |
| elif op == 'src_add': | |
| chosen = target.node | |
| elif op == 'extra_files_add': | |
| chosen = ArrayNode(_symbol('['), ArgumentNode(Token('', tgt_function.filename, 0, 0, 0, None, '[]')), _symbol(']')) | |
| # this is fundamentally error prone | |
| self.interpreter.dataflow_dag.add_edge(chosen, target.node) | |
| extra_files_idnode = IdNode(Token('string', tgt_function.filename, 0, 0, 0, None, 'extra_files')) | |
| if tgt_function not in self.modified_nodes: | |
| self.modified_nodes += [tgt_function] | |
| new_extra_files_node: BaseNode | |
| if target.node.args.get_kwarg_or_default('extra_files', None) is None: | |
| # Target has no extra_files kwarg, create one | |
| new_kwarg_flag = True | |
| new_extra_files_node = chosen | |
| else: | |
| new_kwarg_flag = True | |
| old_extra_files = target.node.args.get_kwarg_or_default('extra_files', None) | |
| target.node.args.kwargs = {k: v for k, v in target.node.args.kwargs.items() if not (isinstance(k, IdNode) and k.value == 'extra_files')} | |
| new_extra_files_node = ArithmeticNode('+', old_extra_files, _symbol('+'), chosen) | |
| tgt_function.args.kwargs[extra_files_idnode] = new_extra_files_node | |
| newfiles_relto = self.get_relto(target.node, chosen) | |
| old_src_list: T.List[T.Any] = flatten([self.interpreter.node_to_runtime_value(sn) for sn in old]) | |
| if op == 'src_add': | |
| name = 'Source' | |
| elif op == 'extra_files_add': | |
| name = 'Extra file' | |
| # Generate the new String nodes | |
| to_append = [] | |
| added = [] | |
| old_src_list = [(target_dir_abs / x).resolve() if isinstance(x, str) else x.to_abs_path(source_root_abs) for x in old_src_list if not isinstance(x, UnknownValue)] | |
| for _newf in sorted(set(newfiles)): | |
| newf = Path(_newf) | |
| if os.path.isabs(newf): | |
| newf = Path(newf) | |
| else: | |
| newf = source_root_abs / newf | |
| if newf in old_src_list: | |
| mlog.log(' -- ', name, mlog.green(str(newf)), 'is already defined for the target --> skipping') | |
| continue | |
| mlog.log(' -- Adding ', name.lower(), mlog.green(str(newf)), 'at', | |
| mlog.yellow(f'{chosen.filename}:{chosen.lineno}')) | |
| added.append(newf) | |
| mocktarget = self.interpreter.funcvals[target.node] | |
| assert isinstance(mocktarget, IntrospectionBuildTarget) | |
| # print("adding ", str(newf), 'to', mocktarget.name) todo: should we write something to stderr? | |
| path = relpath(newf, newfiles_relto) | |
| path = codecs.encode(path, 'unicode_escape').decode() # Because the StringNode constructor does the inverse | |
| token = Token('string', chosen.filename, 0, 0, 0, None, path) | |
| to_append += [StringNode(token)] | |
| assert isinstance(chosen, (FunctionNode, ArrayNode)) | |
| arg_node = chosen.args | |
| # Append to the AST at the right place | |
| arg_node.arguments += to_append | |
| # Mark the node as modified | |
| if chosen not in to_sort_nodes: | |
| to_sort_nodes += [chosen] | |
| # If the extra_files array is newly created, i.e. if new_kwarg_flag is | |
| # True, don't mark it as its parent function node already is, otherwise | |
| # this would cause double modification. | |
| if chosen not in self.modified_nodes and not new_kwarg_flag: | |
| self.modified_nodes += [chosen] | |
| # Utility function to get a list of the sources from a node | |
| def arg_list_from_node(self, n: BaseNode) -> T.List[BaseNode]: | |
| args = [] | |
| if isinstance(n, FunctionNode): | |
| args = list(n.args.arguments) | |
| if n.func_name.value in BUILD_TARGET_FUNCTIONS: | |
| args.pop(0) | |
| elif isinstance(n, ArrayNode): | |
| args = n.args.arguments | |
| elif isinstance(n, ArgumentNode): | |
| args = n.arguments | |
| return args | |
| def rm_src_or_extra(self, op: str, target: IntrospectionBuildTarget, to_be_removed: T.List[str], to_sort_nodes: T.List[T.Union[FunctionNode, ArrayNode]]) -> None: | |
| assert op in {'src_rm', 'extra_files_rm'} | |
| cwd = Path(os.getcwd()) | |
| source_root_abs = cwd / self.interpreter.source_root | |
| # Helper to find the exact string node and its parent | |
| def find_node(src: str) -> T.Tuple[T.Optional[BaseNode], T.Optional[StringNode]]: | |
| if op == 'src_rm': | |
| nodes = self.interpreter.dataflow_dag.reachable(set(target.source_nodes), True).union({target.node}) | |
| elif op == 'extra_files_rm': | |
| nodes = self.interpreter.dataflow_dag.reachable({target.extra_files}, True) | |
| for i in nodes: | |
| if isinstance(i, UnknownValue): | |
| continue | |
| relto = self.get_relto(target.node, i) | |
| if relto is not None: | |
| for j in self.arg_list_from_node(i): | |
| if isinstance(j, StringNode): | |
| if os.path.normpath(relto / j.value) == os.path.normpath(source_root_abs / src): | |
| return i, j | |
| return None, None | |
| if op == 'src_rm': | |
| name = 'source' | |
| elif op == 'extra_files_rm': | |
| name = 'extra file' | |
| for i in to_be_removed: | |
| # Try to find the node with the source string | |
| root, string_node = find_node(i) | |
| if root is None: | |
| mlog.warning(' -- Unable to find', name, mlog.green(i), 'in the target') | |
| continue | |
| if not self.affects_no_other_targets(string_node): | |
| mlog.warning(' -- Removing the', name, mlog.green(i), 'is too compilicated') | |
| continue | |
| if not isinstance(root, (FunctionNode, ArrayNode)): | |
| raise NotImplementedError # I'm lazy | |
| # Remove the found string node from the argument list | |
| arg_node = root.args | |
| mlog.log(' -- Removing', name, mlog.green(i), 'from', | |
| mlog.yellow(f'{string_node.filename}:{string_node.lineno}')) | |
| arg_node.arguments.remove(string_node) | |
| # Mark the node as modified | |
| if root not in to_sort_nodes: | |
| to_sort_nodes += [root] | |
| if root not in self.modified_nodes: | |
| self.modified_nodes += [root] | |
| @RequiredKeys(rewriter_keys['target']) | |
| def process_target(self, cmd: T.Dict[str, T.Any]) -> None: | |
| mlog.log('Processing target', mlog.bold(cmd['target']), 'operation', mlog.cyan(cmd['operation'])) | |
| target = self.find_target(cmd['target']) | |
| if target is None and cmd['operation'] != 'target_add': | |
| mlog.error('Unknown target', mlog.bold(cmd['target']), *self.on_error()) | |
| return self.handle_error() | |
| # Make source paths relative to the current subdir | |
| def rel_source(src: str) -> str: | |
| subdir = os.path.abspath(os.path.join(self.sourcedir, target.subdir)) | |
| if os.path.isabs(src): | |
| return os.path.relpath(src, subdir) | |
| elif not os.path.exists(src): | |
| return src # Trust the user when the source doesn't exist | |
| # Make sure that the path is relative to the subdir | |
| return os.path.relpath(os.path.abspath(src), subdir) | |
| if target is not None: | |
| cmd['sources'] = [rel_source(x) for x in cmd['sources']] | |
| to_sort_nodes: T.List[T.Union[FunctionNode, ArrayNode]] = [] | |
| if cmd['operation'] in {'src_add', 'extra_files_add'}: | |
| self.add_src_or_extra(cmd['operation'], target, cmd['sources'], to_sort_nodes) | |
| elif cmd['operation'] in {'src_rm', 'extra_files_rm'}: | |
| self.rm_src_or_extra(cmd['operation'], target, cmd['sources'], to_sort_nodes) | |
| elif cmd['operation'] == 'target_add': | |
| if target is not None: | |
| mlog.error('Can not add target', mlog.bold(cmd['target']), 'because it already exists', *self.on_error()) | |
| return self.handle_error() | |
| id_base = re.sub(r'[- ]', '_', cmd['target']) | |
| target_id = id_base + '_exe' if cmd['target_type'] == 'executable' else '_lib' | |
| source_id = id_base + '_sources' | |
| filename = os.path.join(os.getcwd(), self.interpreter.source_root, cmd['subdir'], environment.build_filename) | |
| # Build src list | |
| src_arg_node = ArgumentNode(Token('string', filename, 0, 0, 0, None, '')) | |
| src_arr_node = ArrayNode(_symbol('['), src_arg_node, _symbol(']')) | |
| src_far_node = ArgumentNode(Token('string', filename, 0, 0, 0, None, '')) | |
| src_fun_node = FunctionNode(IdNode(Token('id', filename, 0, 0, 0, (0, 0), 'files')), _symbol('('), src_far_node, _symbol(')')) | |
| src_ass_node = AssignmentNode(IdNode(Token('id', filename, 0, 0, 0, (0, 0), source_id)), _symbol('='), src_fun_node) | |
| src_arg_node.arguments = [StringNode(Token('string', filename, 0, 0, 0, None, x)) for x in cmd['sources']] | |
| src_far_node.arguments = [src_arr_node] | |
| # Build target | |
| tgt_arg_node = ArgumentNode(Token('string', filename, 0, 0, 0, None, '')) | |
| tgt_fun_node = FunctionNode(IdNode(Token('id', filename, 0, 0, 0, (0, 0), cmd['target_type'])), _symbol('('), tgt_arg_node, _symbol(')')) | |
| tgt_ass_node = AssignmentNode(IdNode(Token('id', filename, 0, 0, 0, (0, 0), target_id)), _symbol('='), tgt_fun_node) | |
| tgt_arg_node.arguments = [ | |
| StringNode(Token('string', filename, 0, 0, 0, None, cmd['target'])), | |
| IdNode(Token('string', filename, 0, 0, 0, None, source_id)) | |
| ] | |
| src_ass_node.accept(AstIndentationGenerator()) | |
| tgt_ass_node.accept(AstIndentationGenerator()) | |
| self.to_add_nodes += [src_ass_node, tgt_ass_node] | |
| elif cmd['operation'] == 'target_rm': | |
| to_remove: BaseNode = self.find_assignment_node(target.node) | |
| if to_remove is None: | |
| to_remove = target.node | |
| self.to_remove_nodes += [to_remove] | |
| mlog.log(' -- Removing target', mlog.green(cmd['target']), 'at', | |
| mlog.yellow(f'{to_remove.filename}:{to_remove.lineno}')) | |
| elif cmd['operation'] == 'info': | |
| # T.List all sources in the target | |
| cwd = Path(os.getcwd()) | |
| source_root_abs = cwd / self.interpreter.source_root | |
| src_list = self.interpreter.nodes_to_pretty_filelist(source_root_abs, target.subdir, target.source_nodes) | |
| extra_files_list = self.interpreter.nodes_to_pretty_filelist(source_root_abs, target.subdir, [target.extra_files] if target.extra_files else []) | |
| src_list = ['unknown' if isinstance(x, UnknownValue) else relpath(x, source_root_abs) for x in src_list] | |
| extra_files_list = ['unknown' if isinstance(x, UnknownValue) else relpath(x, source_root_abs) for x in extra_files_list] | |
| test_data = { | |
| 'name': target.name, | |
| 'sources': src_list, | |
| 'extra_files': extra_files_list | |
| } | |
| self.add_info('target', target.id, test_data) | |
| # Sort files | |
| for i in to_sort_nodes: | |
| if isinstance(i, FunctionNode) and i.func_name.value in BUILD_TARGET_FUNCTIONS: | |
| src_args = i.args.arguments[1:] | |
| target_name = [i.args.arguments[0]] | |
| else: | |
| src_args = i.args.arguments | |
| target_name = [] | |
| unknown: T.List[BaseNode] = [x for x in src_args if not isinstance(x, StringNode)] | |
| sources: T.List[StringNode] = [x for x in src_args if isinstance(x, StringNode)] | |
| sources = sorted(sources, key=lambda x: pathname_sort_key(x.value)) | |
| i.args.arguments = target_name + unknown + T.cast(T.List[BaseNode], sources) | |
| def process(self, cmd: T.Dict[str, T.Any]) -> None: | |
| 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) -> None: | |
| assert all(hasattr(x, 'lineno') and hasattr(x, 'colno') and hasattr(x, 'filename') for x in self.modified_nodes) | |
| assert all(hasattr(x, 'lineno') and hasattr(x, 'colno') and hasattr(x, 'filename') for x in self.to_remove_nodes) | |
| assert all(isinstance(x, (ArrayNode, FunctionNode)) for x in self.modified_nodes) | |
| assert all(isinstance(x, (ArrayNode, AssignmentNode, FunctionNode)) for x in self.to_remove_nodes) | |
| # Sort based on line and column in reversed order | |
| work_nodes = [{'node': x, 'action': 'modify'} for x in self.modified_nodes] | |
| work_nodes += [{'node': x, 'action': 'rm'} for x in self.to_remove_nodes] | |
| work_nodes = sorted(work_nodes, key=lambda x: (T.cast(BaseNode, x['node']).lineno, T.cast(BaseNode, x['node']).colno), reverse=True) | |
| work_nodes += [{'node': x, 'action': 'add'} for x in self.to_add_nodes] | |
| # Generating the new replacement string | |
| str_list = [] | |
| for i in work_nodes: | |
| new_data = '' | |
| if i['action'] == 'modify' or i['action'] == 'add': | |
| printer = AstPrinter() | |
| T.cast(BaseNode, i['node']).accept(printer) | |
| printer.post_process() | |
| new_data = printer.result.strip() | |
| data = { | |
| 'file': T.cast(BaseNode, i['node']).filename, | |
| 'str': new_data, | |
| 'node': i['node'], | |
| 'action': i['action'] | |
| } | |
| str_list += [data] | |
| # Load build files | |
| files: T.Dict[str, T.Any] = {} | |
| for i in str_list: | |
| if i['file'] in files: | |
| continue | |
| fpath = os.path.realpath(T.cast(str, i['file'])) | |
| fdata = '' | |
| # Create an empty file if it does not exist | |
| if not os.path.exists(fpath): | |
| with open(fpath, 'w', encoding='utf-8'): | |
| pass | |
| with open(fpath, encoding='utf-8') 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[T.cast(str, i['file'])] = { | |
| 'path': fpath, | |
| 'raw': fdata, | |
| 'offsets': line_offsets | |
| } | |
| # Replace in source code | |
| def remove_node(i: T.Dict[str, T.Any]) -> None: | |
| 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, (ArrayNode, FunctionNode)): | |
| end = offsets[node.end_lineno - 1] + node.end_colno | |
| # Only removal is supported for assignments | |
| elif isinstance(node, AssignmentNode) and i['action'] == 'rm': | |
| if isinstance(node.value, (ArrayNode, FunctionNode)): | |
| remove_node({'file': i['file'], 'str': '', 'node': node.value, 'action': 'rm'}) | |
| raw = files[i['file']]['raw'] | |
| while raw[end] != '=': | |
| end += 1 | |
| end += 1 # Handle the '=' | |
| while raw[end] in {' ', '\n', '\t'}: | |
| end += 1 | |
| files[i['file']]['raw'] = raw[:start] + i['str'] + raw[end:] | |
| for i in str_list: | |
| if i['action'] in {'modify', 'rm'}: | |
| remove_node(i) | |
| elif i['action'] == 'add': | |
| files[T.cast(str, i['file'])]['raw'] += T.cast(str, i['str']) + '\n' | |
| # Write the files back | |
| for key, val in files.items(): | |
| mlog.log('Rewriting', mlog.yellow(key)) | |
| with open(val['path'], 'w', encoding='utf-8') as fp: | |
| fp.write(val['raw']) | |
| target_operation_map = { | |
| 'add': 'src_add', | |
| 'rm': 'src_rm', | |
| 'add_target': 'target_add', | |
| 'rm_target': 'target_rm', | |
| 'add_extra_files': 'extra_files_add', | |
| 'rm_extra_files': 'extra_files_rm', | |
| 'info': 'info', | |
| } | |
| def list_to_dict(in_list: T.List[str]) -> T.Dict[str, str]: | |
| result = {} | |
| it = iter(in_list) | |
| try: | |
| for i in it: | |
| # calling next(it) is not a mistake, we're taking the next element from | |
| # the iterator, avoiding the need to preprocess it into a sequence of | |
| # key value pairs. | |
| result[i] = next(it) | |
| except StopIteration: | |
| raise RewriterException('List of key/value pairs must have an even length.') | |
| return result | |
| def list_to_dict_for_delete(args: T.List[str]) -> T.Dict[str, T.Optional[str]]: | |
| if len(args) % 2 == 0 and all(a == '' for a in args[1::2]): | |
| mlog.deprecation('Even-numbered arguments are all blank; ' | |
| 'ignoring these for compatibility with Meson < 1.10') | |
| args = args[::2] | |
| return {a: None for a in args} | |
| def generate_target(options: argparse.Namespace) -> T.List[T.Dict[str, T.Any]]: | |
| return [{ | |
| 'type': 'target', | |
| 'target': options.target, | |
| 'operation': target_operation_map[options.operation], | |
| 'sources': options.sources, | |
| 'subdir': options.subdir, | |
| 'target_type': options.tgt_type, | |
| }] | |
| def generate_kwargs(options: argparse.Namespace) -> T.List[T.Dict[str, T.Any]]: | |
| if options.operation == 'delete': | |
| kwargs = list_to_dict_for_delete(options.kwargs) | |
| else: | |
| kwargs = list_to_dict(options.kwargs) | |
| return [{ | |
| 'type': 'kwargs', | |
| 'function': options.function, | |
| 'id': options.id, | |
| 'operation': options.operation, | |
| 'kwargs': kwargs, | |
| }] | |
| def generate_def_opts(options: argparse.Namespace) -> T.List[T.Dict[str, T.Any]]: | |
| if options.operation == 'delete': | |
| kwargs = list_to_dict_for_delete(options.options) | |
| else: | |
| kwargs = list_to_dict(options.options) | |
| return [{ | |
| 'type': 'default_options', | |
| 'operation': options.operation, | |
| 'options': kwargs, | |
| }] | |
| def generate_cmd(options: argparse.Namespace) -> T.List[T.Dict[str, T.Any]]: | |
| if os.path.exists(options.json): | |
| with open(options.json, encoding='utf-8') as fp: | |
| return T.cast(T.List[T.Dict[str, T.Any]], json.load(fp)) | |
| else: | |
| return T.cast(T.List[T.Dict[str, T.Any]], json.loads(options.json)) | |
| # Map options.type to the actual type name | |
| cli_type_map = { | |
| 'target': generate_target, | |
| 'tgt': generate_target, | |
| 'kwargs': generate_kwargs, | |
| 'default-options': generate_def_opts, | |
| 'def': generate_def_opts, | |
| 'command': generate_cmd, | |
| 'cmd': generate_cmd, | |
| } | |
| def run(options: argparse.Namespace) -> int: | |
| mlog.redirect(True) | |
| if not options.verbose: | |
| mlog.set_quiet() | |
| try: | |
| setup_vsenv() | |
| rewriter = Rewriter(options.sourcedir, skip_errors=options.skip) | |
| rewriter.analyze_meson() | |
| if options.type is None: | |
| mlog.error('No command specified') | |
| return 1 | |
| commands = cli_type_map[options.type](options) | |
| if not isinstance(commands, list): | |
| raise TypeError('Command is not a list') | |
| for i, cmd in enumerate(commands): | |
| if not isinstance(cmd, object): | |
| raise TypeError('Command is not an object') | |
| rewriter.process(cmd) | |
| rewriter.apply_changes() | |
| if i == len(commands) - 1: # Improves the performance, is not necessary for correctness. | |
| break | |
| rewriter.modified_nodes = [] | |
| rewriter.to_remove_nodes = [] | |
| rewriter.to_add_nodes = [] | |
| # The AST changed, so we need to update every information that was derived from the AST | |
| rewriter.interpreter = IntrospectionInterpreter(rewriter.sourcedir, '', rewriter.interpreter.backend, visitors = [AstIDGenerator(), AstIndentationGenerator(), AstConditionLevel()]) | |
| rewriter.analyze_meson() | |
| rewriter.print_info() | |
| return 0 | |
| except Exception as e: | |
| raise e | |
| finally: | |
| mlog.set_verbose() |