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