1#!/usr/local/bin/python3.8 2# Copyright 2016 The Meson development team 3 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7 8# http://www.apache.org/licenses/LICENSE-2.0 9 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16# This class contains the basic functionality needed to run any interpreter 17# or an interpreter-based tool. 18 19# This tool is used to manipulate an existing Meson build definition. 20# 21# - add a file to a target 22# - remove files from a target 23# - move targets 24# - reindent? 25 26from .ast import IntrospectionInterpreter, build_target_functions, AstConditionLevel, AstIDGenerator, AstIndentationGenerator, AstPrinter 27from mesonbuild.mesonlib import MesonException 28from . import mlog, environment 29from functools import wraps 30from .mparser import Token, ArrayNode, ArgumentNode, AssignmentNode, BaseNode, BooleanNode, ElementaryNode, IdNode, FunctionNode, StringNode 31import json, os, re, sys 32import typing as T 33 34class RewriterException(MesonException): 35 pass 36 37def add_arguments(parser, formatter=None): 38 parser.add_argument('-s', '--sourcedir', type=str, default='.', metavar='SRCDIR', help='Path to source directory.') 39 parser.add_argument('-V', '--verbose', action='store_true', default=False, help='Enable verbose output') 40 parser.add_argument('-S', '--skip-errors', dest='skip', action='store_true', default=False, help='Skip errors instead of aborting') 41 subparsers = parser.add_subparsers(dest='type', title='Rewriter commands', description='Rewrite command to execute') 42 43 # Target 44 tgt_parser = subparsers.add_parser('target', help='Modify a target', formatter_class=formatter) 45 tgt_parser.add_argument('-s', '--subdir', default='', dest='subdir', help='Subdirectory of the new target (only for the "add_target" action)') 46 tgt_parser.add_argument('--type', dest='tgt_type', choices=rewriter_keys['target']['target_type'][2], default='executable', 47 help='Type of the target to add (only for the "add_target" action)') 48 tgt_parser.add_argument('target', help='Name or ID of the target') 49 tgt_parser.add_argument('operation', choices=['add', 'rm', 'add_target', 'rm_target', 'info'], 50 help='Action to execute') 51 tgt_parser.add_argument('sources', nargs='*', help='Sources to add/remove') 52 53 # KWARGS 54 kw_parser = subparsers.add_parser('kwargs', help='Modify keyword arguments', formatter_class=formatter) 55 kw_parser.add_argument('operation', choices=rewriter_keys['kwargs']['operation'][2], 56 help='Action to execute') 57 kw_parser.add_argument('function', choices=list(rewriter_func_kwargs.keys()), 58 help='Function type to modify') 59 kw_parser.add_argument('id', help='ID of the function to modify (can be anything for "project")') 60 kw_parser.add_argument('kwargs', nargs='*', help='Pairs of keyword and value') 61 62 # Default options 63 def_parser = subparsers.add_parser('default-options', help='Modify the project default options', formatter_class=formatter) 64 def_parser.add_argument('operation', choices=rewriter_keys['default_options']['operation'][2], 65 help='Action to execute') 66 def_parser.add_argument('options', nargs='*', help='Key, value pairs of configuration option') 67 68 # JSON file/command 69 cmd_parser = subparsers.add_parser('command', help='Execute a JSON array of commands', formatter_class=formatter) 70 cmd_parser.add_argument('json', help='JSON string or file to execute') 71 72class RequiredKeys: 73 def __init__(self, keys): 74 self.keys = keys 75 76 def __call__(self, f): 77 @wraps(f) 78 def wrapped(*wrapped_args, **wrapped_kwargs): 79 assert len(wrapped_args) >= 2 80 cmd = wrapped_args[1] 81 for key, val in self.keys.items(): 82 typ = val[0] # The type of the value 83 default = val[1] # The default value -- None is required 84 choices = val[2] # Valid choices -- None is for everything 85 if key not in cmd: 86 if default is not None: 87 cmd[key] = default 88 else: 89 raise RewriterException('Key "{}" is missing in object for {}' 90 .format(key, f.__name__)) 91 if not isinstance(cmd[key], typ): 92 raise RewriterException('Invalid type of "{}". Required is {} but provided was {}' 93 .format(key, typ.__name__, type(cmd[key]).__name__)) 94 if choices is not None: 95 assert isinstance(choices, list) 96 if cmd[key] not in choices: 97 raise RewriterException('Invalid value of "{}": Possible values are {} but provided was "{}"' 98 .format(key, choices, cmd[key])) 99 return f(*wrapped_args, **wrapped_kwargs) 100 101 return wrapped 102 103class MTypeBase: 104 def __init__(self, node: T.Optional[BaseNode] = None): 105 if node is None: 106 self.node = self._new_node() # lgtm [py/init-calls-subclass] (node creation does not depend on base class state) 107 else: 108 self.node = node 109 self.node_type = None 110 for i in self.supported_nodes(): # lgtm [py/init-calls-subclass] (listing nodes does not depend on base class state) 111 if isinstance(self.node, i): 112 self.node_type = i 113 114 def _new_node(self): 115 # Overwrite in derived class 116 raise RewriterException('Internal error: _new_node of MTypeBase was called') 117 118 def can_modify(self): 119 return self.node_type is not None 120 121 def get_node(self): 122 return self.node 123 124 def supported_nodes(self): 125 # Overwrite in derived class 126 return [] 127 128 def set_value(self, value): 129 # Overwrite in derived class 130 mlog.warning('Cannot set the value of type', mlog.bold(type(self).__name__), '--> skipping') 131 132 def add_value(self, value): 133 # Overwrite in derived class 134 mlog.warning('Cannot add a value of type', mlog.bold(type(self).__name__), '--> skipping') 135 136 def remove_value(self, value): 137 # Overwrite in derived class 138 mlog.warning('Cannot remove a value of type', mlog.bold(type(self).__name__), '--> skipping') 139 140 def remove_regex(self, value): 141 # Overwrite in derived class 142 mlog.warning('Cannot remove a regex in type', mlog.bold(type(self).__name__), '--> skipping') 143 144class MTypeStr(MTypeBase): 145 def __init__(self, node: T.Optional[BaseNode] = None): 146 super().__init__(node) 147 148 def _new_node(self): 149 return StringNode(Token('', '', 0, 0, 0, None, '')) 150 151 def supported_nodes(self): 152 return [StringNode] 153 154 def set_value(self, value): 155 self.node.value = str(value) 156 157class MTypeBool(MTypeBase): 158 def __init__(self, node: T.Optional[BaseNode] = None): 159 super().__init__(node) 160 161 def _new_node(self): 162 return BooleanNode(Token('', '', 0, 0, 0, None, False)) 163 164 def supported_nodes(self): 165 return [BooleanNode] 166 167 def set_value(self, value): 168 self.node.value = bool(value) 169 170class MTypeID(MTypeBase): 171 def __init__(self, node: T.Optional[BaseNode] = None): 172 super().__init__(node) 173 174 def _new_node(self): 175 return IdNode(Token('', '', 0, 0, 0, None, '')) 176 177 def supported_nodes(self): 178 return [IdNode] 179 180 def set_value(self, value): 181 self.node.value = str(value) 182 183class MTypeList(MTypeBase): 184 def __init__(self, node: T.Optional[BaseNode] = None): 185 super().__init__(node) 186 187 def _new_node(self): 188 return ArrayNode(ArgumentNode(Token('', '', 0, 0, 0, None, '')), 0, 0, 0, 0) 189 190 def _new_element_node(self, value): 191 # Overwrite in derived class 192 raise RewriterException('Internal error: _new_element_node of MTypeList was called') 193 194 def _ensure_array_node(self): 195 if not isinstance(self.node, ArrayNode): 196 tmp = self.node 197 self.node = self._new_node() 198 self.node.args.arguments += [tmp] 199 200 def _check_is_equal(self, node, value) -> bool: 201 # Overwrite in derived class 202 return False 203 204 def _check_regex_matches(self, node, regex: str) -> bool: 205 # Overwrite in derived class 206 return False 207 208 def get_node(self): 209 if isinstance(self.node, ArrayNode): 210 if len(self.node.args.arguments) == 1: 211 return self.node.args.arguments[0] 212 return self.node 213 214 def supported_element_nodes(self): 215 # Overwrite in derived class 216 return [] 217 218 def supported_nodes(self): 219 return [ArrayNode] + self.supported_element_nodes() 220 221 def set_value(self, value): 222 if not isinstance(value, list): 223 value = [value] 224 self._ensure_array_node() 225 self.node.args.arguments = [] # Remove all current nodes 226 for i in value: 227 self.node.args.arguments += [self._new_element_node(i)] 228 229 def add_value(self, value): 230 if not isinstance(value, list): 231 value = [value] 232 self._ensure_array_node() 233 for i in value: 234 self.node.args.arguments += [self._new_element_node(i)] 235 236 def _remove_helper(self, value, equal_func): 237 def check_remove_node(node): 238 for j in value: 239 if equal_func(i, j): 240 return True 241 return False 242 243 if not isinstance(value, list): 244 value = [value] 245 self._ensure_array_node() 246 removed_list = [] 247 for i in self.node.args.arguments: 248 if not check_remove_node(i): 249 removed_list += [i] 250 self.node.args.arguments = removed_list 251 252 def remove_value(self, value): 253 self._remove_helper(value, self._check_is_equal) 254 255 def remove_regex(self, regex: str): 256 self._remove_helper(regex, self._check_regex_matches) 257 258class MTypeStrList(MTypeList): 259 def __init__(self, node: T.Optional[BaseNode] = None): 260 super().__init__(node) 261 262 def _new_element_node(self, value): 263 return StringNode(Token('', '', 0, 0, 0, None, str(value))) 264 265 def _check_is_equal(self, node, value) -> bool: 266 if isinstance(node, StringNode): 267 return node.value == value 268 return False 269 270 def _check_regex_matches(self, node, regex: str) -> bool: 271 if isinstance(node, StringNode): 272 return re.match(regex, node.value) is not None 273 return False 274 275 def supported_element_nodes(self): 276 return [StringNode] 277 278class MTypeIDList(MTypeList): 279 def __init__(self, node: T.Optional[BaseNode] = None): 280 super().__init__(node) 281 282 def _new_element_node(self, value): 283 return IdNode(Token('', '', 0, 0, 0, None, str(value))) 284 285 def _check_is_equal(self, node, value) -> bool: 286 if isinstance(node, IdNode): 287 return node.value == value 288 return False 289 290 def _check_regex_matches(self, node, regex: str) -> bool: 291 if isinstance(node, StringNode): 292 return re.match(regex, node.value) is not None 293 return False 294 295 def supported_element_nodes(self): 296 return [IdNode] 297 298rewriter_keys = { 299 'default_options': { 300 'operation': (str, None, ['set', 'delete']), 301 'options': (dict, {}, None) 302 }, 303 'kwargs': { 304 'function': (str, None, None), 305 'id': (str, None, None), 306 'operation': (str, None, ['set', 'delete', 'add', 'remove', 'remove_regex', 'info']), 307 'kwargs': (dict, {}, None) 308 }, 309 'target': { 310 'target': (str, None, None), 311 'operation': (str, None, ['src_add', 'src_rm', 'target_rm', 'target_add', 'info']), 312 'sources': (list, [], None), 313 'subdir': (str, '', None), 314 'target_type': (str, 'executable', ['both_libraries', 'executable', 'jar', 'library', 'shared_library', 'shared_module', 'static_library']), 315 } 316} 317 318rewriter_func_kwargs = { 319 'dependency': { 320 'language': MTypeStr, 321 'method': MTypeStr, 322 'native': MTypeBool, 323 'not_found_message': MTypeStr, 324 'required': MTypeBool, 325 'static': MTypeBool, 326 'version': MTypeStrList, 327 'modules': MTypeStrList 328 }, 329 'target': { 330 'build_by_default': MTypeBool, 331 'build_rpath': MTypeStr, 332 'dependencies': MTypeIDList, 333 'gui_app': MTypeBool, 334 'link_with': MTypeIDList, 335 'export_dynamic': MTypeBool, 336 'implib': MTypeBool, 337 'install': MTypeBool, 338 'install_dir': MTypeStr, 339 'install_rpath': MTypeStr, 340 'pie': MTypeBool 341 }, 342 'project': { 343 'default_options': MTypeStrList, 344 'meson_version': MTypeStr, 345 'license': MTypeStrList, 346 'subproject_dir': MTypeStr, 347 'version': MTypeStr 348 } 349} 350 351class Rewriter: 352 def __init__(self, sourcedir: str, generator: str = 'ninja', skip_errors: bool = False): 353 self.sourcedir = sourcedir 354 self.interpreter = IntrospectionInterpreter(sourcedir, '', generator, visitors = [AstIDGenerator(), AstIndentationGenerator(), AstConditionLevel()]) 355 self.skip_errors = skip_errors 356 self.modified_nodes = [] 357 self.to_remove_nodes = [] 358 self.to_add_nodes = [] 359 self.functions = { 360 'default_options': self.process_default_options, 361 'kwargs': self.process_kwargs, 362 'target': self.process_target, 363 } 364 self.info_dump = None 365 366 def analyze_meson(self): 367 mlog.log('Analyzing meson file:', mlog.bold(os.path.join(self.sourcedir, environment.build_filename))) 368 self.interpreter.analyze() 369 mlog.log(' -- Project:', mlog.bold(self.interpreter.project_data['descriptive_name'])) 370 mlog.log(' -- Version:', mlog.cyan(self.interpreter.project_data['version'])) 371 372 def add_info(self, cmd_type: str, cmd_id: str, data: dict): 373 if self.info_dump is None: 374 self.info_dump = {} 375 if cmd_type not in self.info_dump: 376 self.info_dump[cmd_type] = {} 377 self.info_dump[cmd_type][cmd_id] = data 378 379 def print_info(self): 380 if self.info_dump is None: 381 return 382 sys.stderr.write(json.dumps(self.info_dump, indent=2)) 383 384 def on_error(self): 385 if self.skip_errors: 386 return mlog.cyan('-->'), mlog.yellow('skipping') 387 return mlog.cyan('-->'), mlog.red('aborting') 388 389 def handle_error(self): 390 if self.skip_errors: 391 return None 392 raise MesonException('Rewriting the meson.build failed') 393 394 def find_target(self, target: str): 395 def check_list(name: str) -> T.List[BaseNode]: 396 result = [] 397 for i in self.interpreter.targets: 398 if name == i['name'] or name == i['id']: 399 result += [i] 400 return result 401 402 targets = check_list(target) 403 if targets: 404 if len(targets) == 1: 405 return targets[0] 406 else: 407 mlog.error('There are multiple targets matching', mlog.bold(target)) 408 for i in targets: 409 mlog.error(' -- Target name', mlog.bold(i['name']), 'with ID', mlog.bold(i['id'])) 410 mlog.error('Please try again with the unique ID of the target', *self.on_error()) 411 self.handle_error() 412 return None 413 414 # Check the assignments 415 tgt = None 416 if target in self.interpreter.assignments: 417 node = self.interpreter.assignments[target] 418 if isinstance(node, FunctionNode): 419 if node.func_name in ['executable', 'jar', 'library', 'shared_library', 'shared_module', 'static_library', 'both_libraries']: 420 tgt = self.interpreter.assign_vals[target] 421 422 return tgt 423 424 def find_dependency(self, dependency: str): 425 def check_list(name: str): 426 for i in self.interpreter.dependencies: 427 if name == i['name']: 428 return i 429 return None 430 431 dep = check_list(dependency) 432 if dep is not None: 433 return dep 434 435 # Check the assignments 436 if dependency in self.interpreter.assignments: 437 node = self.interpreter.assignments[dependency] 438 if isinstance(node, FunctionNode): 439 if node.func_name in ['dependency']: 440 name = self.interpreter.flatten_args(node.args)[0] 441 dep = check_list(name) 442 443 return dep 444 445 @RequiredKeys(rewriter_keys['default_options']) 446 def process_default_options(self, cmd): 447 # First, remove the old values 448 kwargs_cmd = { 449 'function': 'project', 450 'id': "/", 451 'operation': 'remove_regex', 452 'kwargs': { 453 'default_options': [f'{x}=.*' for x in cmd['options'].keys()] 454 } 455 } 456 self.process_kwargs(kwargs_cmd) 457 458 # Then add the new values 459 if cmd['operation'] != 'set': 460 return 461 462 kwargs_cmd['operation'] = 'add' 463 kwargs_cmd['kwargs']['default_options'] = [] 464 465 cdata = self.interpreter.coredata 466 options = { 467 **{str(k): v for k, v in cdata.options.items()}, 468 **{str(k): v for k, v in cdata.options.items()}, 469 **{str(k): v for k, v in cdata.options.items()}, 470 **{str(k): v for k, v in cdata.options.items()}, 471 **{str(k): v for k, v in cdata.options.items()}, 472 } 473 474 for key, val in sorted(cmd['options'].items()): 475 if key not in options: 476 mlog.error('Unknown options', mlog.bold(key), *self.on_error()) 477 self.handle_error() 478 continue 479 480 try: 481 val = options[key].validate_value(val) 482 except MesonException as e: 483 mlog.error('Unable to set', mlog.bold(key), mlog.red(str(e)), *self.on_error()) 484 self.handle_error() 485 continue 486 487 kwargs_cmd['kwargs']['default_options'] += [f'{key}={val}'] 488 489 self.process_kwargs(kwargs_cmd) 490 491 @RequiredKeys(rewriter_keys['kwargs']) 492 def process_kwargs(self, cmd): 493 mlog.log('Processing function type', mlog.bold(cmd['function']), 'with id', mlog.cyan("'" + cmd['id'] + "'")) 494 if cmd['function'] not in rewriter_func_kwargs: 495 mlog.error('Unknown function type', cmd['function'], *self.on_error()) 496 return self.handle_error() 497 kwargs_def = rewriter_func_kwargs[cmd['function']] 498 499 # Find the function node to modify 500 node = None 501 arg_node = None 502 if cmd['function'] == 'project': 503 # msys bash may expand '/' to a path. It will mangle '//' to '/' 504 # but in order to keep usage shell-agnostic, also allow `//` as 505 # the function ID such that it will work in both msys bash and 506 # other shells. 507 if {'/', '//'}.isdisjoint({cmd['id']}): 508 mlog.error('The ID for the function type project must be "/" or "//" not "' + cmd['id'] + '"', *self.on_error()) 509 return self.handle_error() 510 node = self.interpreter.project_node 511 arg_node = node.args 512 elif cmd['function'] == 'target': 513 tmp = self.find_target(cmd['id']) 514 if tmp: 515 node = tmp['node'] 516 arg_node = node.args 517 elif cmd['function'] == 'dependency': 518 tmp = self.find_dependency(cmd['id']) 519 if tmp: 520 node = tmp['node'] 521 arg_node = node.args 522 if not node: 523 mlog.error('Unable to find the function node') 524 assert isinstance(node, FunctionNode) 525 assert isinstance(arg_node, ArgumentNode) 526 # Transform the key nodes to plain strings 527 arg_node.kwargs = {k.value: v for k, v in arg_node.kwargs.items()} 528 529 # Print kwargs info 530 if cmd['operation'] == 'info': 531 info_data = {} 532 for key, val in sorted(arg_node.kwargs.items()): 533 info_data[key] = None 534 if isinstance(val, ElementaryNode): 535 info_data[key] = val.value 536 elif isinstance(val, ArrayNode): 537 data_list = [] 538 for i in val.args.arguments: 539 element = None 540 if isinstance(i, ElementaryNode): 541 element = i.value 542 data_list += [element] 543 info_data[key] = data_list 544 545 self.add_info('kwargs', '{}#{}'.format(cmd['function'], cmd['id']), info_data) 546 return # Nothing else to do 547 548 # Modify the kwargs 549 num_changed = 0 550 for key, val in sorted(cmd['kwargs'].items()): 551 if key not in kwargs_def: 552 mlog.error('Cannot modify unknown kwarg', mlog.bold(key), *self.on_error()) 553 self.handle_error() 554 continue 555 556 # Remove the key from the kwargs 557 if cmd['operation'] == 'delete': 558 if key in arg_node.kwargs: 559 mlog.log(' -- Deleting', mlog.bold(key), 'from the kwargs') 560 del arg_node.kwargs[key] 561 num_changed += 1 562 else: 563 mlog.log(' -- Key', mlog.bold(key), 'is already deleted') 564 continue 565 566 if key not in arg_node.kwargs: 567 arg_node.kwargs[key] = None 568 modifyer = kwargs_def[key](arg_node.kwargs[key]) 569 if not modifyer.can_modify(): 570 mlog.log(' -- Skipping', mlog.bold(key), 'because it is to complex to modify') 571 572 # Apply the operation 573 val_str = str(val) 574 if cmd['operation'] == 'set': 575 mlog.log(' -- Setting', mlog.bold(key), 'to', mlog.yellow(val_str)) 576 modifyer.set_value(val) 577 elif cmd['operation'] == 'add': 578 mlog.log(' -- Adding', mlog.yellow(val_str), 'to', mlog.bold(key)) 579 modifyer.add_value(val) 580 elif cmd['operation'] == 'remove': 581 mlog.log(' -- Removing', mlog.yellow(val_str), 'from', mlog.bold(key)) 582 modifyer.remove_value(val) 583 elif cmd['operation'] == 'remove_regex': 584 mlog.log(' -- Removing all values matching', mlog.yellow(val_str), 'from', mlog.bold(key)) 585 modifyer.remove_regex(val) 586 587 # Write back the result 588 arg_node.kwargs[key] = modifyer.get_node() 589 num_changed += 1 590 591 # Convert the keys back to IdNode's 592 arg_node.kwargs = {IdNode(Token('', '', 0, 0, 0, None, k)): v for k, v in arg_node.kwargs.items()} 593 if num_changed > 0 and node not in self.modified_nodes: 594 self.modified_nodes += [node] 595 596 def find_assignment_node(self, node: BaseNode) -> AssignmentNode: 597 if node.ast_id and node.ast_id in self.interpreter.reverse_assignment: 598 return self.interpreter.reverse_assignment[node.ast_id] 599 return None 600 601 @RequiredKeys(rewriter_keys['target']) 602 def process_target(self, cmd): 603 mlog.log('Processing target', mlog.bold(cmd['target']), 'operation', mlog.cyan(cmd['operation'])) 604 target = self.find_target(cmd['target']) 605 if target is None and cmd['operation'] != 'target_add': 606 mlog.error('Unknown target', mlog.bold(cmd['target']), *self.on_error()) 607 return self.handle_error() 608 609 # Make source paths relative to the current subdir 610 def rel_source(src: str) -> str: 611 subdir = os.path.abspath(os.path.join(self.sourcedir, target['subdir'])) 612 if os.path.isabs(src): 613 return os.path.relpath(src, subdir) 614 elif not os.path.exists(src): 615 return src # Trust the user when the source doesn't exist 616 # Make sure that the path is relative to the subdir 617 return os.path.relpath(os.path.abspath(src), subdir) 618 619 if target is not None: 620 cmd['sources'] = [rel_source(x) for x in cmd['sources']] 621 622 # Utility function to get a list of the sources from a node 623 def arg_list_from_node(n): 624 args = [] 625 if isinstance(n, FunctionNode): 626 args = list(n.args.arguments) 627 if n.func_name in build_target_functions: 628 args.pop(0) 629 elif isinstance(n, ArrayNode): 630 args = n.args.arguments 631 elif isinstance(n, ArgumentNode): 632 args = n.arguments 633 return args 634 635 to_sort_nodes = [] 636 637 if cmd['operation'] == 'src_add': 638 node = None 639 if target['sources']: 640 node = target['sources'][0] 641 else: 642 node = target['node'] 643 assert node is not None 644 645 # Generate the current source list 646 src_list = [] 647 for i in target['sources']: 648 for j in arg_list_from_node(i): 649 if isinstance(j, StringNode): 650 src_list += [j.value] 651 652 # Generate the new String nodes 653 to_append = [] 654 for i in sorted(set(cmd['sources'])): 655 if i in src_list: 656 mlog.log(' -- Source', mlog.green(i), 'is already defined for the target --> skipping') 657 continue 658 mlog.log(' -- Adding source', mlog.green(i), 'at', 659 mlog.yellow(f'{node.filename}:{node.lineno}')) 660 token = Token('string', node.filename, 0, 0, 0, None, i) 661 to_append += [StringNode(token)] 662 663 # Append to the AST at the right place 664 arg_node = None 665 if isinstance(node, (FunctionNode, ArrayNode)): 666 arg_node = node.args 667 elif isinstance(node, ArgumentNode): 668 arg_node = node 669 assert arg_node is not None 670 arg_node.arguments += to_append 671 672 # Mark the node as modified 673 if arg_node not in to_sort_nodes and not isinstance(node, FunctionNode): 674 to_sort_nodes += [arg_node] 675 if node not in self.modified_nodes: 676 self.modified_nodes += [node] 677 678 elif cmd['operation'] == 'src_rm': 679 # Helper to find the exact string node and its parent 680 def find_node(src): 681 for i in target['sources']: 682 for j in arg_list_from_node(i): 683 if isinstance(j, StringNode): 684 if j.value == src: 685 return i, j 686 return None, None 687 688 for i in cmd['sources']: 689 # Try to find the node with the source string 690 root, string_node = find_node(i) 691 if root is None: 692 mlog.warning(' -- Unable to find source', mlog.green(i), 'in the target') 693 continue 694 695 # Remove the found string node from the argument list 696 arg_node = None 697 if isinstance(root, (FunctionNode, ArrayNode)): 698 arg_node = root.args 699 elif isinstance(root, ArgumentNode): 700 arg_node = root 701 assert arg_node is not None 702 mlog.log(' -- Removing source', mlog.green(i), 'from', 703 mlog.yellow(f'{string_node.filename}:{string_node.lineno}')) 704 arg_node.arguments.remove(string_node) 705 706 # Mark the node as modified 707 if arg_node not in to_sort_nodes and not isinstance(root, FunctionNode): 708 to_sort_nodes += [arg_node] 709 if root not in self.modified_nodes: 710 self.modified_nodes += [root] 711 712 elif cmd['operation'] == 'target_add': 713 if target is not None: 714 mlog.error('Can not add target', mlog.bold(cmd['target']), 'because it already exists', *self.on_error()) 715 return self.handle_error() 716 717 id_base = re.sub(r'[- ]', '_', cmd['target']) 718 target_id = id_base + '_exe' if cmd['target_type'] == 'executable' else '_lib' 719 source_id = id_base + '_sources' 720 filename = os.path.join(cmd['subdir'], environment.build_filename) 721 722 # Build src list 723 src_arg_node = ArgumentNode(Token('string', filename, 0, 0, 0, None, '')) 724 src_arr_node = ArrayNode(src_arg_node, 0, 0, 0, 0) 725 src_far_node = ArgumentNode(Token('string', filename, 0, 0, 0, None, '')) 726 src_fun_node = FunctionNode(filename, 0, 0, 0, 0, 'files', src_far_node) 727 src_ass_node = AssignmentNode(filename, 0, 0, source_id, src_fun_node) 728 src_arg_node.arguments = [StringNode(Token('string', filename, 0, 0, 0, None, x)) for x in cmd['sources']] 729 src_far_node.arguments = [src_arr_node] 730 731 # Build target 732 tgt_arg_node = ArgumentNode(Token('string', filename, 0, 0, 0, None, '')) 733 tgt_fun_node = FunctionNode(filename, 0, 0, 0, 0, cmd['target_type'], tgt_arg_node) 734 tgt_ass_node = AssignmentNode(filename, 0, 0, target_id, tgt_fun_node) 735 tgt_arg_node.arguments = [ 736 StringNode(Token('string', filename, 0, 0, 0, None, cmd['target'])), 737 IdNode(Token('string', filename, 0, 0, 0, None, source_id)) 738 ] 739 740 src_ass_node.accept(AstIndentationGenerator()) 741 tgt_ass_node.accept(AstIndentationGenerator()) 742 self.to_add_nodes += [src_ass_node, tgt_ass_node] 743 744 elif cmd['operation'] == 'target_rm': 745 to_remove = self.find_assignment_node(target['node']) 746 if to_remove is None: 747 to_remove = target['node'] 748 self.to_remove_nodes += [to_remove] 749 mlog.log(' -- Removing target', mlog.green(cmd['target']), 'at', 750 mlog.yellow(f'{to_remove.filename}:{to_remove.lineno}')) 751 752 elif cmd['operation'] == 'info': 753 # T.List all sources in the target 754 src_list = [] 755 for i in target['sources']: 756 for j in arg_list_from_node(i): 757 if isinstance(j, StringNode): 758 src_list += [j.value] 759 test_data = { 760 'name': target['name'], 761 'sources': src_list 762 } 763 self.add_info('target', target['id'], test_data) 764 765 # Sort files 766 for i in to_sort_nodes: 767 convert = lambda text: int(text) if text.isdigit() else text.lower() 768 alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)] 769 path_sorter = lambda key: ([(key.count('/') <= idx, alphanum_key(x)) for idx, x in enumerate(key.split('/'))]) 770 771 unknown = [x for x in i.arguments if not isinstance(x, StringNode)] 772 sources = [x for x in i.arguments if isinstance(x, StringNode)] 773 sources = sorted(sources, key=lambda x: path_sorter(x.value)) 774 i.arguments = unknown + sources 775 776 def process(self, cmd): 777 if 'type' not in cmd: 778 raise RewriterException('Command has no key "type"') 779 if cmd['type'] not in self.functions: 780 raise RewriterException('Unknown command "{}". Supported commands are: {}' 781 .format(cmd['type'], list(self.functions.keys()))) 782 self.functions[cmd['type']](cmd) 783 784 def apply_changes(self): 785 assert all(hasattr(x, 'lineno') and hasattr(x, 'colno') and hasattr(x, 'filename') for x in self.modified_nodes) 786 assert all(hasattr(x, 'lineno') and hasattr(x, 'colno') and hasattr(x, 'filename') for x in self.to_remove_nodes) 787 assert all(isinstance(x, (ArrayNode, FunctionNode)) for x in self.modified_nodes) 788 assert all(isinstance(x, (ArrayNode, AssignmentNode, FunctionNode)) for x in self.to_remove_nodes) 789 # Sort based on line and column in reversed order 790 work_nodes = [{'node': x, 'action': 'modify'} for x in self.modified_nodes] 791 work_nodes += [{'node': x, 'action': 'rm'} for x in self.to_remove_nodes] 792 work_nodes = list(sorted(work_nodes, key=lambda x: (x['node'].lineno, x['node'].colno), reverse=True)) 793 work_nodes += [{'node': x, 'action': 'add'} for x in self.to_add_nodes] 794 795 # Generating the new replacement string 796 str_list = [] 797 for i in work_nodes: 798 new_data = '' 799 if i['action'] == 'modify' or i['action'] == 'add': 800 printer = AstPrinter() 801 i['node'].accept(printer) 802 printer.post_process() 803 new_data = printer.result.strip() 804 data = { 805 'file': i['node'].filename, 806 'str': new_data, 807 'node': i['node'], 808 'action': i['action'] 809 } 810 str_list += [data] 811 812 # Load build files 813 files = {} 814 for i in str_list: 815 if i['file'] in files: 816 continue 817 fpath = os.path.realpath(os.path.join(self.sourcedir, i['file'])) 818 fdata = '' 819 # Create an empty file if it does not exist 820 if not os.path.exists(fpath): 821 with open(fpath, 'w', encoding='utf-8'): 822 pass 823 with open(fpath, encoding='utf-8') as fp: 824 fdata = fp.read() 825 826 # Generate line offsets numbers 827 m_lines = fdata.splitlines(True) 828 offset = 0 829 line_offsets = [] 830 for j in m_lines: 831 line_offsets += [offset] 832 offset += len(j) 833 834 files[i['file']] = { 835 'path': fpath, 836 'raw': fdata, 837 'offsets': line_offsets 838 } 839 840 # Replace in source code 841 def remove_node(i): 842 offsets = files[i['file']]['offsets'] 843 raw = files[i['file']]['raw'] 844 node = i['node'] 845 line = node.lineno - 1 846 col = node.colno 847 start = offsets[line] + col 848 end = start 849 if isinstance(node, (ArrayNode, FunctionNode)): 850 end = offsets[node.end_lineno - 1] + node.end_colno 851 852 # Only removal is supported for assignments 853 elif isinstance(node, AssignmentNode) and i['action'] == 'rm': 854 if isinstance(node.value, (ArrayNode, FunctionNode)): 855 remove_node({'file': i['file'], 'str': '', 'node': node.value, 'action': 'rm'}) 856 raw = files[i['file']]['raw'] 857 while raw[end] != '=': 858 end += 1 859 end += 1 # Handle the '=' 860 while raw[end] in [' ', '\n', '\t']: 861 end += 1 862 863 files[i['file']]['raw'] = raw[:start] + i['str'] + raw[end:] 864 865 for i in str_list: 866 if i['action'] in ['modify', 'rm']: 867 remove_node(i) 868 elif i['action'] in ['add']: 869 files[i['file']]['raw'] += i['str'] + '\n' 870 871 # Write the files back 872 for key, val in files.items(): 873 mlog.log('Rewriting', mlog.yellow(key)) 874 with open(val['path'], 'w', encoding='utf-8') as fp: 875 fp.write(val['raw']) 876 877target_operation_map = { 878 'add': 'src_add', 879 'rm': 'src_rm', 880 'add_target': 'target_add', 881 'rm_target': 'target_rm', 882 'info': 'info', 883} 884 885def list_to_dict(in_list: T.List[str]) -> T.Dict[str, str]: 886 result = {} 887 it = iter(in_list) 888 try: 889 for i in it: 890 # calling next(it) is not a mistake, we're taking the next element from 891 # the iterator, avoiding the need to preprocess it into a sequence of 892 # key value pairs. 893 result[i] = next(it) 894 except StopIteration: 895 raise TypeError('in_list parameter of list_to_dict must have an even length.') 896 return result 897 898def generate_target(options) -> T.List[dict]: 899 return [{ 900 'type': 'target', 901 'target': options.target, 902 'operation': target_operation_map[options.operation], 903 'sources': options.sources, 904 'subdir': options.subdir, 905 'target_type': options.tgt_type, 906 }] 907 908def generate_kwargs(options) -> T.List[dict]: 909 return [{ 910 'type': 'kwargs', 911 'function': options.function, 912 'id': options.id, 913 'operation': options.operation, 914 'kwargs': list_to_dict(options.kwargs), 915 }] 916 917def generate_def_opts(options) -> T.List[dict]: 918 return [{ 919 'type': 'default_options', 920 'operation': options.operation, 921 'options': list_to_dict(options.options), 922 }] 923 924def generate_cmd(options) -> T.List[dict]: 925 if os.path.exists(options.json): 926 with open(options.json, encoding='utf-8') as fp: 927 return json.load(fp) 928 else: 929 return json.loads(options.json) 930 931# Map options.type to the actual type name 932cli_type_map = { 933 'target': generate_target, 934 'tgt': generate_target, 935 'kwargs': generate_kwargs, 936 'default-options': generate_def_opts, 937 'def': generate_def_opts, 938 'command': generate_cmd, 939 'cmd': generate_cmd, 940} 941 942def run(options): 943 if not options.verbose: 944 mlog.set_quiet() 945 946 try: 947 rewriter = Rewriter(options.sourcedir, skip_errors=options.skip) 948 rewriter.analyze_meson() 949 950 if options.type is None: 951 mlog.error('No command specified') 952 return 1 953 954 commands = cli_type_map[options.type](options) 955 956 if not isinstance(commands, list): 957 raise TypeError('Command is not a list') 958 959 for i in commands: 960 if not isinstance(i, object): 961 raise TypeError('Command is not an object') 962 rewriter.process(i) 963 964 rewriter.apply_changes() 965 rewriter.print_info() 966 return 0 967 except Exception as e: 968 raise e 969 finally: 970 mlog.set_verbose() 971