diff options
| -rw-r--r-- | mesonbuild/astinterpreter.py | 241 | ||||
| -rw-r--r-- | mesonbuild/interpreter.py | 603 | ||||
| -rw-r--r-- | mesonbuild/interpreterbase.py | 636 | ||||
| -rw-r--r-- | mesonbuild/mparser.py | 127 | ||||
| -rw-r--r-- | mesonbuild/optinterpreter.py | 2 | ||||
| -rwxr-xr-x | mesonrewriter.py | 64 | ||||
| -rwxr-xr-x | run_unittests.py | 58 | ||||
| -rw-r--r-- | test cases/rewrite/1 basic/added.txt | 5 | ||||
| -rw-r--r-- | test cases/rewrite/1 basic/meson.build | 5 | ||||
| -rw-r--r-- | test cases/rewrite/1 basic/removed.txt | 5 | ||||
| -rw-r--r-- | test cases/rewrite/2 subdirs/meson.build | 5 | ||||
| -rw-r--r-- | test cases/rewrite/2 subdirs/sub1/after.txt | 1 | ||||
| -rw-r--r-- | test cases/rewrite/2 subdirs/sub1/meson.build | 1 | ||||
| -rw-r--r-- | test cases/rewrite/2 subdirs/sub2/meson.build | 2 | 
14 files changed, 1113 insertions, 642 deletions
diff --git a/mesonbuild/astinterpreter.py b/mesonbuild/astinterpreter.py new file mode 100644 index 000000000..3691d64ee --- /dev/null +++ b/mesonbuild/astinterpreter.py @@ -0,0 +1,241 @@ +# Copyright 2016 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +#     http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This class contains the basic functionality needed to run any interpreter +# or an interpreter-based tool. + +from . import interpreterbase, mlog, mparser, mesonlib +from . import environment + +from .interpreterbase import InterpreterException, InvalidArguments + +import os, sys + +class DontCareObject(interpreterbase.InterpreterObject): +    pass + +class MockExecutable(interpreterbase.InterpreterObject): +    pass + +class MockStaticLibrary(interpreterbase.InterpreterObject): +    pass + +class MockSharedLibrary(interpreterbase.InterpreterObject): +    pass + +class MockCustomTarget(interpreterbase.InterpreterObject): +    pass + +class MockRunTarget(interpreterbase.InterpreterObject): +    pass + +ADD_SOURCE = 0 +REMOVE_SOURCE = 1 + +class AstInterpreter(interpreterbase.InterpreterBase): +    def __init__(self, source_root, subdir): +        super().__init__(source_root, subdir) +        self.asts = {} +        self.funcs.update({'project' : self.func_do_nothing, +                           'test' : self.func_do_nothing, +                           'benchmark' : self.func_do_nothing, +                           'install_headers' : self.func_do_nothing, +                           'install_man' : self.func_do_nothing, +                           'install_data' : self.func_do_nothing, +                           'install_subdir' : self.func_do_nothing, +                           'configuration_data' : self.func_do_nothing, +                           'configure_file' : self.func_do_nothing, +                           'find_program' : self.func_do_nothing, +                           'include_directories' : self.func_do_nothing, +                           'add_global_arguments' : self.func_do_nothing, +                           'add_global_link_arguments' : self.func_do_nothing, +                           'add_project_arguments' : self.func_do_nothing, +                           'add_project_link_arguments' : self.func_do_nothing, +                           'message' : self.func_do_nothing, +                           'generator' : self.func_do_nothing, +                           'error' : self.func_do_nothing, +                           'run_command' : self.func_do_nothing, +                           'assert' : self.func_do_nothing, +                           'subproject' : self.func_do_nothing, +                           'dependency' : self.func_do_nothing, +                           'get_option' : self.func_do_nothing, +                           'join_paths' : self.func_do_nothing, +                           'environment' : self.func_do_nothing, +                           'import' : self.func_do_nothing, +                           'vcs_tag' : self.func_do_nothing, +                           'add_languages' : self.func_do_nothing, +                           'declare_dependency' : self.func_do_nothing, +                           'files' : self.func_files, +                           'executable': self.func_executable, +                           'static_library' : self.func_static_lib, +                           'shared_library' : self.func_shared_lib, +                           'library' : self.func_library, +                           'build_target' : self.func_build_target, +                           'custom_target' : self.func_custom_target, +                           'run_target' : self.func_run_target, +                           'subdir' : self.func_subdir, +                           'set_variable' : self.func_set_variable, +                           'get_variable' : self.func_get_variable, +                           'is_variable' : self.func_is_variable, +                           }) + +    def func_do_nothing(self, node, args, kwargs): +        return True + +    def method_call(self, node): +        return True + +    def func_executable(self, node, args, kwargs): +        if args[0] == self.targetname: +            if self.operation == ADD_SOURCE: +                self.add_source_to_target(node, args, kwargs) +            elif self.operation == REMOVE_SOURCE: +                self.remove_source_from_target(node, args, kwargs) +            else: +                raise NotImplementedError('Bleep bloop') +        return MockExecutable() + +    def func_static_lib(self, node, args, kwargs): +        return MockStaticLibrary() + +    def func_shared_lib(self, node, args, kwargs): +        return MockSharedLibrary() + +    def func_library(self, node, args, kwargs): +        return self.func_shared_lib(node, args, kwargs) + +    def func_custom_target(self, node, args, kwargs): +        return MockCustomTarget() + +    def func_run_target(self, node, args, kwargs): +        return MockRunTarget() + +    def func_subdir(self, node, args, kwargs): +        prev_subdir = self.subdir +        subdir = os.path.join(prev_subdir, args[0]) +        self.subdir = subdir +        buildfilename = os.path.join(self.subdir, environment.build_filename) +        absname = os.path.join(self.source_root, buildfilename) +        if not os.path.isfile(absname): +            self.subdir = prev_subdir +            raise InterpreterException('Nonexistant build def file %s.' % buildfilename) +        with open(absname, encoding='utf8') as f: +            code = f.read() +        assert(isinstance(code, str)) +        try: +            codeblock = mparser.Parser(code, self.subdir).parse() +            self.asts[subdir] = codeblock +        except mesonlib.MesonException as me: +            me.file = buildfilename +            raise me +        self.evaluate_codeblock(codeblock) +        self.subdir = prev_subdir + +    def func_files(self, node, args, kwargs): +        if not isinstance(args, list): +            return [args] +        return args + +    def evaluate_arithmeticstatement(self, cur): +        return 0 + +    def evaluate_plusassign(self, node): +        return 0 + +    def evaluate_indexing(self, node): +        return 0 + +    def reduce_arguments(self, args): +        assert(isinstance(args, mparser.ArgumentNode)) +        if args.incorrect_order(): +            raise InvalidArguments('All keyword arguments must be after positional arguments.') +        return (args.arguments, args.kwargs) + +    def transform(self): +        self.load_root_meson_file() +        self.asts[''] = self.ast +        self.sanity_check_ast() +        self.parse_project() +        self.run() + +    def add_source(self, targetname, filename): +        self.operation = ADD_SOURCE +        self.targetname = targetname +        self.filename = filename +        self.transform() + +    def remove_source(self, targetname, filename): +        self.operation = REMOVE_SOURCE +        self.targetname = targetname +        self.filename = filename +        self.transform() + +    def unknown_function_called(self, func_name): +        mlog.warning('Unknown function called: ' + func_name) + +    def add_source_to_target(self, node, args, kwargs): +        namespan = node.args.arguments[0].bytespan +        buildfilename = os.path.join(self.source_root, self.subdir, environment.build_filename) +        raw_data = open(buildfilename, 'r').read() +        updated = raw_data[0:namespan[1]] + (", '%s'" % self.filename) + raw_data[namespan[1]:] +        open(buildfilename, 'w').write(updated) +        sys.exit(0) + +    def remove_argument_item(self, args, i): +        assert(isinstance(args, mparser.ArgumentNode)) +        namespan = args.arguments[i].bytespan +        # Usually remove the comma after this item but if it is +        # the last argument, we need to remove the one before. +        if i >= len(args.commas): +            i -= 1 +        if i < 0: +            commaspan = (0, 0) # Removed every entry in the list. +        else: +            commaspan = args.commas[i].bytespan +        if commaspan[0] < namespan[0]: +            commaspan, namespan = namespan, commaspan +        buildfilename = os.path.join(self.source_root, args.subdir, environment.build_filename) +        raw_data = open(buildfilename, 'r').read() +        intermediary = raw_data[0:commaspan[0]] + raw_data[commaspan[1]:] +        updated = intermediary[0:namespan[0]] + intermediary[namespan[1]:] +        open(buildfilename, 'w').write(updated) +        sys.exit(0) + +    def hacky_find_and_remove(self, node_to_remove): +        for a in self.asts[node_to_remove.subdir].lines: +            if a.lineno == node_to_remove.lineno: +                if isinstance(a, mparser.AssignmentNode): +                    v = a.value +                    if not isinstance(v, mparser.ArrayNode): +                        raise NotImplementedError('Not supported yet, bro.') +                    args = v.args +                    for i in range(len(args.arguments)): +                        if isinstance(args.arguments[i], mparser.StringNode) and self.filename == args.arguments[i].value: +                            self.remove_argument_item(args, i) +                raise NotImplementedError('Sukkess') + +    def remove_source_from_target(self, node, args, kwargs): +        for i in range(1, len(node.args)): +            # Is file name directly in function call as a string. +            if isinstance(node.args.arguments[i], mparser.StringNode) and self.filename == node.args.arguments[i].value: +                self.remove_argument_item(node.args, i) +            # Is file name in a variable that gets expanded here. +            if isinstance(node.args.arguments[i], mparser.IdNode): +                avar = self.get_variable(node.args.arguments[i].value) +                if not isinstance(avar, list): +                    raise NotImplementedError('Non-arrays not supported yet, sorry.') +                for entry in avar: +                    if isinstance(entry, mparser.StringNode) and entry.value == self.filename: +                        self.hacky_find_and_remove(entry) +        sys.exit('Could not find source %s in target %s.' % (self.filename, args[0])) diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index e9273e4d2..2167b813b 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -22,58 +22,17 @@ from . import optinterpreter  from . import compilers  from .wrap import wrap  from . import mesonlib +from mesonbuild.interpreterbase import InterpreterBase +from mesonbuild.interpreterbase import check_stringlist, noPosargs, noKwargs, stringArgs +from mesonbuild.interpreterbase import InterpreterException, InvalidArguments, InvalidCode +from mesonbuild.interpreterbase import InterpreterObject, MutableInterpreterObject  import os, sys, subprocess, shutil, uuid, re -from functools import wraps  import importlib -import copy  run_depr_printed = False -class InterpreterException(mesonlib.MesonException): -    pass - -class InvalidCode(InterpreterException): -    pass - -class InvalidArguments(InterpreterException): -    pass - -# Decorators for method calls. - -def check_stringlist(a, msg='Arguments must be strings.'): -    if not isinstance(a, list): -        mlog.debug('Not a list:', str(a)) -        raise InvalidArguments('Argument not a list.') -    if not all(isinstance(s, str) for s in a): -        mlog.debug('Element not a string:', str(a)) -        raise InvalidArguments(msg) - -def noPosargs(f): -    @wraps(f) -    def wrapped(self, node, args, kwargs): -        if len(args) != 0: -            raise InvalidArguments('Function does not take positional arguments.') -        return f(self, node, args, kwargs) -    return wrapped - -def noKwargs(f): -    @wraps(f) -    def wrapped(self, node, args, kwargs): -        if len(kwargs) != 0: -            raise InvalidArguments('Function does not take keyword arguments.') -        return f(self, node, args, kwargs) -    return wrapped - -def stringArgs(f): -    @wraps(f) -    def wrapped(self, node, args, kwargs): -        assert(isinstance(args, list)) -        check_stringlist(args) -        return f(self, node, args, kwargs) -    return wrapped -  def stringifyUserArguments(args):      if isinstance(args, list):          return '[%s]' % ', '.join([stringifyUserArguments(x) for x in args]) @@ -83,18 +42,6 @@ def stringifyUserArguments(args):          return "'%s'" % args      raise InvalidArguments('Function accepts only strings, integers, lists and lists thereof.') -class InterpreterObject(): -    def __init__(self): -        self.methods = {} - -    def method_call(self, method_name, args, kwargs): -        if method_name in self.methods: -            return self.methods[method_name](args, kwargs) -        raise InvalidCode('Unknown method "%s" in object.' % method_name) - -class MutableInterpreterObject(InterpreterObject): -    def __init__(self): -        super().__init__()  class TryRunResultHolder(InterpreterObject):      def __init__(self, res): @@ -1139,16 +1086,15 @@ class MesonMain(InterpreterObject):                  return args[1]              raise InterpreterException('Unknown cross property: %s.' % propname) -class Interpreter(): +class Interpreter(InterpreterBase):      def __init__(self, build, backend, subproject='', subdir='', subproject_dir='subprojects'): +        super().__init__(build.environment.get_source_dir(), subdir)          self.build = build          self.environment = build.environment          self.coredata = self.environment.get_coredata()          self.backend = backend          self.subproject = subproject -        self.subdir = subdir -        self.source_root = build.environment.get_source_dir()          self.subproject_dir = subproject_dir          option_file = os.path.join(self.source_root, self.subdir, 'meson_options.txt')          if os.path.exists(option_file): @@ -1156,22 +1102,9 @@ class Interpreter():                                                    self.build.environment.cmd_line_options.projectoptions)              oi.process(option_file)              self.build.environment.merge_options(oi.options) -        mesonfile = os.path.join(self.source_root, self.subdir, environment.build_filename) -        if not os.path.isfile(mesonfile): -            raise InvalidArguments('Missing Meson file in %s' % mesonfile) -        with open(mesonfile, encoding='utf8') as mf: -            code = mf.read() -        if len(code.strip()) == 0: -            raise InvalidCode('Builder file is empty.') -        assert(isinstance(code, str)) -        try: -            self.ast = mparser.Parser(code).parse() -        except mesonlib.MesonException as me: -            me.file = environment.build_filename -            raise me +        self.load_root_meson_file()          self.sanity_check_ast() -        self.variables = {} -        self.builtin = {'meson': MesonMain(build, self)} +        self.builtin.update({'meson': MesonMain(build, self)})          self.generators = []          self.visited_subdirs = {}          self.args_frozen = False @@ -1196,7 +1129,7 @@ class Interpreter():          self.build_def_files = [os.path.join(self.subdir, environment.build_filename)]      def build_func_dict(self): -        self.funcs = {'project' : self.func_project, +        self.funcs.update({'project' : self.func_project,                        'message' : self.func_message,                        'error' : self.func_error,                        'executable': self.func_executable, @@ -1241,14 +1174,7 @@ class Interpreter():                        'assert': self.func_assert,                        'environment' : self.func_environment,                        'join_paths' : self.func_join_paths, -                     } - -    def parse_project(self): -        """ -        Parses project() and initializes languages, compilers etc. Do this -        early because we need this before we parse the rest of the AST. -        """ -        self.evaluate_codeblock(self.ast, end=1) +                     })      def module_method_callback(self, invalues):          unwrap_single = False @@ -1295,16 +1221,6 @@ class Interpreter():      def get_variables(self):          return self.variables -    def sanity_check_ast(self): -        if not isinstance(self.ast, mparser.CodeBlockNode): -            raise InvalidCode('AST is of invalid type. Possibly a bug in the parser.') -        if len(self.ast.lines) == 0: -            raise InvalidCode('No statements in code.') -        first = self.ast.lines[0] -        if not isinstance(first, mparser.FunctionNode) or first.func_name != 'project': -            raise InvalidCode('First statement must be a call to project') - -      def check_cross_stdlibs(self):          if self.build.environment.is_cross_build():              cross_info = self.build.environment.cross_info @@ -1321,71 +1237,6 @@ class Interpreter():                  except KeyError as e:                      pass -    def run(self): -        # Evaluate everything after the first line, which is project() because -        # we already parsed that in self.parse_project() -        self.evaluate_codeblock(self.ast, start=1) -        mlog.log('Build targets in project:', mlog.bold(str(len(self.build.targets)))) - -    def evaluate_codeblock(self, node, start=0, end=None): -        if node is None: -            return -        if not isinstance(node, mparser.CodeBlockNode): -            e = InvalidCode('Tried to execute a non-codeblock. Possibly a bug in the parser.') -            e.lineno = node.lineno -            e.colno = node.colno -            raise e -        statements = node.lines[start:end] -        i = 0 -        while i < len(statements): -            cur = statements[i] -            try: -                self.evaluate_statement(cur) -            except Exception as e: -                if not(hasattr(e, 'lineno')): -                    e.lineno = cur.lineno -                    e.colno = cur.colno -                    e.file = os.path.join(self.subdir, 'meson.build') -                raise e -            i += 1 # In THE FUTURE jump over blocks and stuff. - -    def get_variable(self, varname): -        if varname in self.builtin: -            return self.builtin[varname] -        if varname in self.variables: -            return self.variables[varname] -        raise InvalidCode('Unknown variable "%s".' % varname) - -    def func_set_variable(self, node, args, kwargs): -        if len(args) != 2: -            raise InvalidCode('Set_variable takes two arguments.') -        varname = args[0] -        value = self.to_native(args[1]) -        self.set_variable(varname, value) - -    @noKwargs -    def func_get_variable(self, node, args, kwargs): -        if len(args)<1 or len(args)>2: -            raise InvalidCode('Get_variable takes one or two arguments.') -        varname = args[0] -        if not isinstance(varname, str): -            raise InterpreterException('First argument must be a string.') -        try: -            return self.variables[varname] -        except KeyError: -            pass -        if len(args) == 2: -            return args[1] -        raise InterpreterException('Tried to get unknown variable "%s".' % varname) - -    @stringArgs -    @noKwargs -    def func_is_variable(self, node, args, kwargs): -        if len(args) != 1: -            raise InvalidCode('Is_variable takes two arguments.') -        varname = args[0] -        return varname in self.variables -      @stringArgs      @noKwargs      def func_import(self, node, args, kwargs): @@ -1446,63 +1297,6 @@ class Interpreter():          if not value:              raise InterpreterException('Assert failed: ' + message) -    def set_variable(self, varname, variable): -        if variable is None: -            raise InvalidCode('Can not assign None to variable.') -        if not isinstance(varname, str): -            raise InvalidCode('First argument to set_variable must be a string.') -        if not self.is_assignable(variable): -            raise InvalidCode('Assigned value not of assignable type.') -        if re.match('[_a-zA-Z][_0-9a-zA-Z]*$', varname) is None: -            raise InvalidCode('Invalid variable name: ' + varname) -        if varname in self.builtin: -            raise InvalidCode('Tried to overwrite internal variable "%s"' % varname) -        self.variables[varname] = variable - -    def evaluate_statement(self, cur): -        if isinstance(cur, mparser.FunctionNode): -            return self.function_call(cur) -        elif isinstance(cur, mparser.AssignmentNode): -            return self.assignment(cur) -        elif isinstance(cur, mparser.MethodNode): -            return self.method_call(cur) -        elif isinstance(cur, mparser.StringNode): -            return cur.value -        elif isinstance(cur, mparser.BooleanNode): -            return cur.value -        elif isinstance(cur, mparser.IfClauseNode): -            return self.evaluate_if(cur) -        elif isinstance(cur, mparser.IdNode): -            return self.get_variable(cur.value) -        elif isinstance(cur, mparser.ComparisonNode): -            return self.evaluate_comparison(cur) -        elif isinstance(cur, mparser.ArrayNode): -            return self.evaluate_arraystatement(cur) -        elif isinstance(cur, mparser.NumberNode): -            return cur.value -        elif isinstance(cur, mparser.AndNode): -            return self.evaluate_andstatement(cur) -        elif isinstance(cur, mparser.OrNode): -            return self.evaluate_orstatement(cur) -        elif isinstance(cur, mparser.NotNode): -            return self.evaluate_notstatement(cur) -        elif isinstance(cur, mparser.UMinusNode): -            return self.evaluate_uminusstatement(cur) -        elif isinstance(cur, mparser.ArithmeticNode): -            return self.evaluate_arithmeticstatement(cur) -        elif isinstance(cur, mparser.ForeachClauseNode): -            return self.evaluate_foreach(cur) -        elif isinstance(cur, mparser.PlusAssignmentNode): -            return self.evaluate_plusassign(cur) -        elif isinstance(cur, mparser.IndexNode): -            return self.evaluate_indexing(cur) -        elif isinstance(cur, mparser.TernaryNode): -            return self.evaluate_ternary(cur) -        elif self.is_elementary_type(cur): -            return cur -        else: -            raise InvalidCode("Unknown statement.") -      def validate_arguments(self, args, argcount, arg_types):          if argcount is not None:              if argcount != len(args): @@ -2193,7 +1987,7 @@ requirements use the version keyword argument instead.''')              code = f.read()          assert(isinstance(code, str))          try: -            codeblock = mparser.Parser(code).parse() +            codeblock = mparser.Parser(code, self.subdir).parse()          except mesonlib.MesonException as me:              me.file = buildfilename              raise me @@ -2376,21 +2170,9 @@ requirements use the version keyword argument instead.''')      def func_join_paths(self, node, args, kwargs):          return os.path.join(*args).replace('\\', '/') -    def flatten(self, args): -        if isinstance(args, mparser.StringNode): -            return args.value -        if isinstance(args, (int, str, InterpreterObject)): -            return args -        result = [] -        for a in args: -            if isinstance(a, list): -                rest = self.flatten(a) -                result = result + rest -            elif isinstance(a, mparser.StringNode): -                result.append(a.value) -            else: -                result.append(a) -        return result +    def run(self): +        super().run() +        mlog.log('Build targets in project:', mlog.bold(str(len(self.build.targets))))      def source_strings_to_files(self, sources):          results = [] @@ -2487,145 +2269,6 @@ requirements use the version keyword argument instead.''')              if not os.path.isfile(fname):                  raise InterpreterException('Tried to add non-existing source file %s.' % s) -    def function_call(self, node): -        func_name = node.func_name -        (posargs, kwargs) = self.reduce_arguments(node.args) -        if func_name in self.funcs: -            return self.funcs[func_name](node, self.flatten(posargs), kwargs) -        else: -            raise InvalidCode('Unknown function "%s".' % func_name) - -    def is_assignable(self, value): -        return isinstance(value, (InterpreterObject, dependencies.Dependency, -                                  str, int, list, mesonlib.File)) - -    def assignment(self, node): -        assert(isinstance(node, mparser.AssignmentNode)) -        var_name = node.var_name -        if not isinstance(var_name, str): -            raise InvalidArguments('Tried to assign value to a non-variable.') -        value = self.evaluate_statement(node.value) -        value = self.to_native(value) -        if not self.is_assignable(value): -            raise InvalidCode('Tried to assign an invalid value to variable.') -        # For mutable objects we need to make a copy on assignment -        if isinstance(value, MutableInterpreterObject): -            value = copy.deepcopy(value) -        self.set_variable(var_name, value) -        return value - -    def reduce_arguments(self, args): -        assert(isinstance(args, mparser.ArgumentNode)) -        if args.incorrect_order(): -            raise InvalidArguments('All keyword arguments must be after positional arguments.') -        reduced_pos = [self.evaluate_statement(arg) for arg in args.arguments] -        reduced_kw = {} -        for key in args.kwargs.keys(): -            if not isinstance(key, str): -                raise InvalidArguments('Keyword argument name is not a string.') -            a = args.kwargs[key] -            reduced_kw[key] = self.evaluate_statement(a) -        if not isinstance(reduced_pos, list): -            reduced_pos = [reduced_pos] -        return (reduced_pos, reduced_kw) - -    def bool_method_call(self, obj, method_name, args): -        obj = self.to_native(obj) -        (posargs, _) = self.reduce_arguments(args) -        if method_name == 'to_string': -            if len(posargs) == 0: -                if obj == True: -                    return 'true' -                else: -                    return 'false' -            elif len(posargs) == 2 and isinstance(posargs[0], str) and isinstance(posargs[1], str): -                if obj == True: -                    return posargs[0] -                else: -                    return posargs[1] -            else: -                raise InterpreterException('bool.to_string() must have either no arguments or exactly two string arguments that signify what values to return for true and false.') -        elif method_name == 'to_int': -            if obj == True: -                return 1 -            else: -                return 0 -        else: -            raise InterpreterException('Unknown method "%s" for a boolean.' % method_name) - -    def int_method_call(self, obj, method_name, args): -        obj = self.to_native(obj) -        (posargs, _) = self.reduce_arguments(args) -        if method_name == 'is_even': -            if len(posargs) == 0: -                return obj % 2 == 0 -            else: -                raise InterpreterException('int.is_even() must have no arguments.') -        elif method_name == 'is_odd': -            if len(posargs) == 0: -                return obj % 2 != 0 -            else: -                raise InterpreterException('int.is_odd() must have no arguments.') -        else: -            raise InterpreterException('Unknown method "%s" for an integer.' % method_name) - -    def string_method_call(self, obj, method_name, args): -        obj = self.to_native(obj) -        (posargs, _) = self.reduce_arguments(args) -        if method_name == 'strip': -            return obj.strip() -        elif method_name == 'format': -            return self.format_string(obj, args) -        elif method_name == 'to_upper': -            return obj.upper() -        elif method_name == 'to_lower': -            return obj.lower() -        elif method_name == 'underscorify': -            return re.sub(r'[^a-zA-Z0-9]', '_', obj) -        elif method_name == 'split': -            if len(posargs) > 1: -                raise InterpreterException('Split() must have at most one argument.') -            elif len(posargs) == 1: -                s = posargs[0] -                if not isinstance(s, str): -                    raise InterpreterException('Split() argument must be a string') -                return obj.split(s) -            else: -                return obj.split() -        elif method_name == 'startswith' or method_name == 'contains' or method_name == 'endswith': -            s = posargs[0] -            if not isinstance(s, str): -                raise InterpreterException('Argument must be a string.') -            if method_name == 'startswith': -                return obj.startswith(s) -            elif method_name == 'contains': -                return obj.find(s) >= 0 -            return obj.endswith(s) -        elif method_name == 'to_int': -            try: -                return int(obj) -            except Exception: -                raise InterpreterException('String {!r} cannot be converted to int'.format(obj)) -        elif method_name == 'join': -            if len(posargs) != 1: -                raise InterpreterException('Join() takes exactly one argument.') -            strlist = posargs[0] -            check_stringlist(strlist) -            return obj.join(strlist) -        elif method_name == 'version_compare': -            if len(posargs) != 1: -                raise InterpreterException('Version_compare() takes exactly one argument.') -            cmpr = posargs[0] -            if not isinstance(cmpr, str): -                raise InterpreterException('Version_compare() argument must be a string.') -            return mesonlib.version_compare(obj, cmpr) -        raise InterpreterException('Unknown method "%s" for a string.' % method_name) - -    def to_native(self, arg): -        if isinstance(arg, (mparser.StringNode, mparser.NumberNode, -                            mparser.BooleanNode)): -            return arg.value -        return arg      def format_string(self, templ, args):          templ = self.to_native(templ) @@ -2638,32 +2281,6 @@ requirements use the version keyword argument instead.''')              templ = templ.replace('@{}@'.format(i), str(arg))          return templ -    def method_call(self, node): -        invokable = node.source_object -        if isinstance(invokable, mparser.IdNode): -            object_name = invokable.value -            obj = self.get_variable(object_name) -        else: -            obj = self.evaluate_statement(invokable) -        method_name = node.name -        args = node.args -        if isinstance(obj, mparser.StringNode): -            obj = obj.get_value() -        if isinstance(obj, str): -            return self.string_method_call(obj, method_name, args) -        if isinstance(obj, bool): -            return self.bool_method_call(obj, method_name, args) -        if isinstance(obj, int): -            return self.int_method_call(obj, method_name, args) -        if isinstance(obj, list): -            return self.array_method_call(obj, method_name, self.reduce_arguments(args)[0]) -        if not isinstance(obj, InterpreterObject): -            raise InvalidArguments('Variable "%s" is not callable.' % object_name) -        (args, kwargs) = self.reduce_arguments(args) -        if method_name == 'extract_objects': -            self.validate_extraction(obj.held_object) -        return obj.method_call(method_name, self.flatten(args), kwargs) -      # Only permit object extraction from the same subproject      def validate_extraction(self, buildtarget):          if not self.subdir.startswith(self.subproject_dir): @@ -2675,20 +2292,6 @@ requirements use the version keyword argument instead.''')              if self.subdir.split('/')[1] != buildtarget.subdir.split('/')[1]:                  raise InterpreterException('Tried to extract objects from a different subproject.') -    def array_method_call(self, obj, method_name, args): -        if method_name == 'contains': -            return self.check_contains(obj, args) -        elif method_name == 'length': -            return len(obj) -        elif method_name == 'get': -            index = args[0] -            if not isinstance(index, int): -                raise InvalidArguments('Array index must be a number.') -            if index < -len(obj) or index >= len(obj): -                raise InvalidArguments('Array index %s is out of bounds for array of size %d.' % (index, len(obj))) -            return obj[index] -        raise InterpreterException('Arrays do not have a method called "%s".' % method_name) -      def check_contains(self, obj, args):          if len(args) != 1:              raise InterpreterException('Contains method takes exactly one argument.') @@ -2705,183 +2308,5 @@ requirements use the version keyword argument instead.''')                  pass          return False -    def evaluate_if(self, node): -        assert(isinstance(node, mparser.IfClauseNode)) -        for i in node.ifs: -            result = self.evaluate_statement(i.condition) -            if not(isinstance(result, bool)): -                raise InvalidCode('If clause {!r} does not evaluate to true or false.'.format(result)) -            if result: -                self.evaluate_codeblock(i.block) -                return -        if not isinstance(node.elseblock, mparser.EmptyNode): -            self.evaluate_codeblock(node.elseblock) - -    def evaluate_ternary(self, node): -        assert(isinstance(node, mparser.TernaryNode)) -        result = self.evaluate_statement(node.condition) -        if not isinstance(result, bool): -            raise InterpreterException('Ternary condition is not boolean.') -        if result: -            return self.evaluate_statement(node.trueblock) -        else: -            return self.evaluate_statement(node.falseblock) - -    def evaluate_foreach(self, node): -        assert(isinstance(node, mparser.ForeachClauseNode)) -        varname = node.varname.value -        items = self.evaluate_statement(node.items) -        if not isinstance(items, list): -            raise InvalidArguments('Items of foreach loop is not an array') -        for item in items: -            self.set_variable(varname, item) -            self.evaluate_codeblock(node.block) - -    def evaluate_plusassign(self, node): -        assert(isinstance(node, mparser.PlusAssignmentNode)) -        varname = node.var_name -        addition = self.evaluate_statement(node.value) -        # Remember that all variables are immutable. We must always create a -        # full new variable and then assign it. -        old_variable = self.get_variable(varname) -        if isinstance(old_variable, str): -          if not isinstance(addition, str): -            raise InvalidArguments('The += operator requires a string on the right hand side if the variable on the left is a string') -          new_value = old_variable + addition -        elif isinstance(old_variable, int): -          if not isinstance(addition, int): -            raise InvalidArguments('The += operator requires an int on the right hand side if the variable on the left is an int') -          new_value = old_variable + addition -        elif not isinstance(old_variable, list): -            raise InvalidArguments('The += operator currently only works with arrays, strings or ints ') -        # Add other data types here. -        else: -            if isinstance(addition, list): -                new_value = old_variable + addition -            else: -                new_value = old_variable + [addition] -        self.set_variable(varname, new_value) - -    def evaluate_indexing(self, node): -        assert(isinstance(node, mparser.IndexNode)) -        iobject = self.evaluate_statement(node.iobject) -        if not isinstance(iobject, list): -            raise InterpreterException('Tried to index a non-array object.') -        index = self.evaluate_statement(node.index) -        if not isinstance(index, int): -            raise InterpreterException('Index value is not an integer.') -        if index < -len(iobject) or index >= len(iobject): -            raise InterpreterException('Index %d out of bounds of array of size %d.' % (index, len(iobject))) -        return iobject[index] - -    def is_elementary_type(self, v): -        return isinstance(v, (int, float, str, bool, list)) - -    def evaluate_comparison(self, node): -        v1 = self.evaluate_statement(node.left) -        v2 = self.evaluate_statement(node.right) -        if self.is_elementary_type(v1): -            val1 = v1 -        else: -            val1 = v1.value -        if self.is_elementary_type(v2): -            val2 = v2 -        else: -            val2 = v2.value -        if node.ctype == '==': -            return val1 == val2 -        elif node.ctype == '!=': -            return val1 != val2 -        elif node.ctype == '<': -            return val1 < val2 -        elif node.ctype == '<=': -            return val1 <= val2 -        elif node.ctype == '>': -            return val1 > val2 -        elif node.ctype == '>=': -            return val1 >= val2 -        else: -            raise InvalidCode('You broke my compare eval.') - -    def evaluate_andstatement(self, cur): -        l = self.evaluate_statement(cur.left) -        if isinstance(l, mparser.BooleanNode): -            l = l.value -        if not isinstance(l, bool): -            raise InterpreterException('First argument to "and" is not a boolean.') -        if not l: -            return False -        r = self.evaluate_statement(cur.right) -        if isinstance(r, mparser.BooleanNode): -            r = r.value -        if not isinstance(r, bool): -            raise InterpreterException('Second argument to "and" is not a boolean.') -        return r - -    def evaluate_orstatement(self, cur): -        l = self.evaluate_statement(cur.left) -        if isinstance(l, mparser.BooleanNode): -            l = l.get_value() -        if not isinstance(l, bool): -            raise InterpreterException('First argument to "or" is not a boolean.') -        if l: -            return True -        r = self.evaluate_statement(cur.right) -        if isinstance(r, mparser.BooleanNode): -            r = r.get_value() -        if not isinstance(r, bool): -            raise InterpreterException('Second argument to "or" is not a boolean.') -        return r - -    def evaluate_notstatement(self, cur): -        v = self.evaluate_statement(cur.value) -        if isinstance(v, mparser.BooleanNode): -            v = v.value -        if not isinstance(v, bool): -            raise InterpreterException('Argument to "not" is not a boolean.') -        return not v - -    def evaluate_uminusstatement(self, cur): -        v = self.evaluate_statement(cur.value) -        if isinstance(v, mparser.NumberNode): -            v = v.value -        if not isinstance(v, int): -            raise InterpreterException('Argument to negation is not an integer.') -        return -v - -    def evaluate_arithmeticstatement(self, cur): -        l = self.to_native(self.evaluate_statement(cur.left)) -        r = self.to_native(self.evaluate_statement(cur.right)) - -        if cur.operation == 'add': -            try: -                return l + r -            except Exception as e: -                raise InvalidCode('Invalid use of addition: ' + str(e)) -        elif cur.operation == 'sub': -            if not isinstance(l, int) or not isinstance(r, int): -                raise InvalidCode('Subtraction works only with integers.') -            return l - r -        elif cur.operation == 'mul': -            if not isinstance(l, int) or not isinstance(r, int): -                raise InvalidCode('Multiplication works only with integers.') -            return l * r -        elif cur.operation == 'div': -            if not isinstance(l, int) or not isinstance(r, int): -                raise InvalidCode('Division works only with integers.') -            return l // r -        elif cur.operation == 'mod': -            if not isinstance(l, int) or not isinstance(r, int): -                raise InvalidCode('Modulo works only with integers.') -            return l % r -        else: -            raise InvalidCode('You broke me.') - -    def evaluate_arraystatement(self, cur): -        (arguments, kwargs) = self.reduce_arguments(cur.args) -        if len(kwargs) > 0: -            raise InvalidCode('Keyword arguments are invalid in array construction.') -        return arguments -      def is_subproject(self):          return self.subproject != '' diff --git a/mesonbuild/interpreterbase.py b/mesonbuild/interpreterbase.py new file mode 100644 index 000000000..97814f48b --- /dev/null +++ b/mesonbuild/interpreterbase.py @@ -0,0 +1,636 @@ +# Copyright 2016 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +#     http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This class contains the basic functionality needed to run any interpreter +# or an interpreter-based tool. + +from . import mparser, mesonlib, mlog +from . import environment, dependencies + +import os, copy, re +from functools import wraps + +# Decorators for method calls. + +def check_stringlist(a, msg='Arguments must be strings.'): +    if not isinstance(a, list): +        mlog.debug('Not a list:', str(a)) +        raise InvalidArguments('Argument not a list.') +    if not all(isinstance(s, str) for s in a): +        mlog.debug('Element not a string:', str(a)) +        raise InvalidArguments(msg) + +def noPosargs(f): +    @wraps(f) +    def wrapped(self, node, args, kwargs): +        if len(args) != 0: +            raise InvalidArguments('Function does not take positional arguments.') +        return f(self, node, args, kwargs) +    return wrapped + +def noKwargs(f): +    @wraps(f) +    def wrapped(self, node, args, kwargs): +        if len(kwargs) != 0: +            raise InvalidArguments('Function does not take keyword arguments.') +        return f(self, node, args, kwargs) +    return wrapped + +def stringArgs(f): +    @wraps(f) +    def wrapped(self, node, args, kwargs): +        assert(isinstance(args, list)) +        check_stringlist(args) +        return f(self, node, args, kwargs) +    return wrapped + + +class InterpreterException(mesonlib.MesonException): +    pass + +class InvalidCode(InterpreterException): +    pass + +class InvalidArguments(InterpreterException): +    pass + +class InterpreterObject(): +    def __init__(self): +        self.methods = {} + +    def method_call(self, method_name, args, kwargs): +        if method_name in self.methods: +            return self.methods[method_name](args, kwargs) +        raise InvalidCode('Unknown method "%s" in object.' % method_name) + +class MutableInterpreterObject(InterpreterObject): +    def __init__(self): +        super().__init__() + + +class InterpreterBase: +    def __init__(self, source_root, subdir): +        self.source_root = source_root +        self.funcs = {} +        self.builtin = {} +        self.subdir = subdir +        self.variables = {} + +    def load_root_meson_file(self): +        mesonfile = os.path.join(self.source_root, self.subdir, environment.build_filename) +        if not os.path.isfile(mesonfile): +            raise InvalidArguments('Missing Meson file in %s' % mesonfile) +        with open(mesonfile, encoding='utf8') as mf: +            code = mf.read() +        if len(code.strip()) == 0: +            raise InvalidCode('Builder file is empty.') +        assert(isinstance(code, str)) +        try: +            self.ast = mparser.Parser(code, self.subdir).parse() +        except mesonlib.MesonException as me: +            me.file = environment.build_filename +            raise me + +    def parse_project(self): +        """ +        Parses project() and initializes languages, compilers etc. Do this +        early because we need this before we parse the rest of the AST. +        """ +        self.evaluate_codeblock(self.ast, end=1) + +    def sanity_check_ast(self): +        if not isinstance(self.ast, mparser.CodeBlockNode): +            raise InvalidCode('AST is of invalid type. Possibly a bug in the parser.') +        if len(self.ast.lines) == 0: +            raise InvalidCode('No statements in code.') +        first = self.ast.lines[0] +        if not isinstance(first, mparser.FunctionNode) or first.func_name != 'project': +            raise InvalidCode('First statement must be a call to project') + +    def run(self): +        # Evaluate everything after the first line, which is project() because +        # we already parsed that in self.parse_project() +        self.evaluate_codeblock(self.ast, start=1) + +    def evaluate_codeblock(self, node, start=0, end=None): +        if node is None: +            return +        if not isinstance(node, mparser.CodeBlockNode): +            e = InvalidCode('Tried to execute a non-codeblock. Possibly a bug in the parser.') +            e.lineno = node.lineno +            e.colno = node.colno +            raise e +        statements = node.lines[start:end] +        i = 0 +        while i < len(statements): +            cur = statements[i] +            try: +                self.evaluate_statement(cur) +            except Exception as e: +                if not(hasattr(e, 'lineno')): +                    e.lineno = cur.lineno +                    e.colno = cur.colno +                    e.file = os.path.join(self.subdir, 'meson.build') +                raise e +            i += 1 # In THE FUTURE jump over blocks and stuff. + +    def evaluate_statement(self, cur): +        if isinstance(cur, mparser.FunctionNode): +            return self.function_call(cur) +        elif isinstance(cur, mparser.AssignmentNode): +            return self.assignment(cur) +        elif isinstance(cur, mparser.MethodNode): +            return self.method_call(cur) +        elif isinstance(cur, mparser.StringNode): +            return cur.value +        elif isinstance(cur, mparser.BooleanNode): +            return cur.value +        elif isinstance(cur, mparser.IfClauseNode): +            return self.evaluate_if(cur) +        elif isinstance(cur, mparser.IdNode): +            return self.get_variable(cur.value) +        elif isinstance(cur, mparser.ComparisonNode): +            return self.evaluate_comparison(cur) +        elif isinstance(cur, mparser.ArrayNode): +            return self.evaluate_arraystatement(cur) +        elif isinstance(cur, mparser.NumberNode): +            return cur.value +        elif isinstance(cur, mparser.AndNode): +            return self.evaluate_andstatement(cur) +        elif isinstance(cur, mparser.OrNode): +            return self.evaluate_orstatement(cur) +        elif isinstance(cur, mparser.NotNode): +            return self.evaluate_notstatement(cur) +        elif isinstance(cur, mparser.UMinusNode): +            return self.evaluate_uminusstatement(cur) +        elif isinstance(cur, mparser.ArithmeticNode): +            return self.evaluate_arithmeticstatement(cur) +        elif isinstance(cur, mparser.ForeachClauseNode): +            return self.evaluate_foreach(cur) +        elif isinstance(cur, mparser.PlusAssignmentNode): +            return self.evaluate_plusassign(cur) +        elif isinstance(cur, mparser.IndexNode): +            return self.evaluate_indexing(cur) +        elif isinstance(cur, mparser.TernaryNode): +            return self.evaluate_ternary(cur) +        elif self.is_elementary_type(cur): +            return cur +        else: +            raise InvalidCode("Unknown statement.") + +    def evaluate_arraystatement(self, cur): +        (arguments, kwargs) = self.reduce_arguments(cur.args) +        if len(kwargs) > 0: +            raise InvalidCode('Keyword arguments are invalid in array construction.') +        return arguments + +    def evaluate_notstatement(self, cur): +        v = self.evaluate_statement(cur.value) +        if isinstance(v, mparser.BooleanNode): +            v = v.value +        if not isinstance(v, bool): +            raise InterpreterException('Argument to "not" is not a boolean.') +        return not v + +    def evaluate_if(self, node): +        assert(isinstance(node, mparser.IfClauseNode)) +        for i in node.ifs: +            result = self.evaluate_statement(i.condition) +            if not(isinstance(result, bool)): +                raise InvalidCode('If clause {!r} does not evaluate to true or false.'.format(result)) +            if result: +                self.evaluate_codeblock(i.block) +                return +        if not isinstance(node.elseblock, mparser.EmptyNode): +            self.evaluate_codeblock(node.elseblock) + +    def evaluate_comparison(self, node): +        v1 = self.evaluate_statement(node.left) +        v2 = self.evaluate_statement(node.right) +        if self.is_elementary_type(v1): +            val1 = v1 +        else: +            val1 = v1.value +        if self.is_elementary_type(v2): +            val2 = v2 +        else: +            val2 = v2.value +        if node.ctype == '==': +            return val1 == val2 +        elif node.ctype == '!=': +            return val1 != val2 +        elif node.ctype == '<': +            return val1 < val2 +        elif node.ctype == '<=': +            return val1 <= val2 +        elif node.ctype == '>': +            return val1 > val2 +        elif node.ctype == '>=': +            return val1 >= val2 +        else: +            raise InvalidCode('You broke my compare eval.') + +    def evaluate_andstatement(self, cur): +        l = self.evaluate_statement(cur.left) +        if isinstance(l, mparser.BooleanNode): +            l = l.value +        if not isinstance(l, bool): +            raise InterpreterException('First argument to "and" is not a boolean.') +        if not l: +            return False +        r = self.evaluate_statement(cur.right) +        if isinstance(r, mparser.BooleanNode): +            r = r.value +        if not isinstance(r, bool): +            raise InterpreterException('Second argument to "and" is not a boolean.') +        return r + +    def evaluate_orstatement(self, cur): +        l = self.evaluate_statement(cur.left) +        if isinstance(l, mparser.BooleanNode): +            l = l.get_value() +        if not isinstance(l, bool): +            raise InterpreterException('First argument to "or" is not a boolean.') +        if l: +            return True +        r = self.evaluate_statement(cur.right) +        if isinstance(r, mparser.BooleanNode): +            r = r.get_value() +        if not isinstance(r, bool): +            raise InterpreterException('Second argument to "or" is not a boolean.') +        return r + +    def evaluate_uminusstatement(self, cur): +        v = self.evaluate_statement(cur.value) +        if isinstance(v, mparser.NumberNode): +            v = v.value +        if not isinstance(v, int): +            raise InterpreterException('Argument to negation is not an integer.') +        return -v + +    def evaluate_arithmeticstatement(self, cur): +        l = self.to_native(self.evaluate_statement(cur.left)) +        r = self.to_native(self.evaluate_statement(cur.right)) + +        if cur.operation == 'add': +            try: +                return l + r +            except Exception as e: +                raise InvalidCode('Invalid use of addition: ' + str(e)) +        elif cur.operation == 'sub': +            if not isinstance(l, int) or not isinstance(r, int): +                raise InvalidCode('Subtraction works only with integers.') +            return l - r +        elif cur.operation == 'mul': +            if not isinstance(l, int) or not isinstance(r, int): +                raise InvalidCode('Multiplication works only with integers.') +            return l * r +        elif cur.operation == 'div': +            if not isinstance(l, int) or not isinstance(r, int): +                raise InvalidCode('Division works only with integers.') +            return l // r +        elif cur.operation == 'mod': +            if not isinstance(l, int) or not isinstance(r, int): +                raise InvalidCode('Modulo works only with integers.') +            return l % r +        else: +            raise InvalidCode('You broke me.') + +    def evaluate_ternary(self, node): +        assert(isinstance(node, mparser.TernaryNode)) +        result = self.evaluate_statement(node.condition) +        if not isinstance(result, bool): +            raise InterpreterException('Ternary condition is not boolean.') +        if result: +            return self.evaluate_statement(node.trueblock) +        else: +            return self.evaluate_statement(node.falseblock) + +    def evaluate_foreach(self, node): +        assert(isinstance(node, mparser.ForeachClauseNode)) +        varname = node.varname.value +        items = self.evaluate_statement(node.items) +        if not isinstance(items, list): +            raise InvalidArguments('Items of foreach loop is not an array') +        for item in items: +            self.set_variable(varname, item) +            self.evaluate_codeblock(node.block) + +    def evaluate_plusassign(self, node): +        assert(isinstance(node, mparser.PlusAssignmentNode)) +        varname = node.var_name +        addition = self.evaluate_statement(node.value) +        # Remember that all variables are immutable. We must always create a +        # full new variable and then assign it. +        old_variable = self.get_variable(varname) +        if isinstance(old_variable, str): +            if not isinstance(addition, str): +                raise InvalidArguments('The += operator requires a string on the right hand side if the variable on the left is a string') +            new_value = old_variable + addition +        elif isinstance(old_variable, int): +            if not isinstance(addition, int): +                raise InvalidArguments('The += operator requires an int on the right hand side if the variable on the left is an int') +            new_value = old_variable + addition +        elif not isinstance(old_variable, list): +            raise InvalidArguments('The += operator currently only works with arrays, strings or ints ') +        # Add other data types here. +        else: +            if isinstance(addition, list): +                new_value = old_variable + addition +            else: +                new_value = old_variable + [addition] +        self.set_variable(varname, new_value) + +    def evaluate_indexing(self, node): +        assert(isinstance(node, mparser.IndexNode)) +        iobject = self.evaluate_statement(node.iobject) +        if not isinstance(iobject, list): +            raise InterpreterException('Tried to index a non-array object.') +        index = self.evaluate_statement(node.index) +        if not isinstance(index, int): +            raise InterpreterException('Index value is not an integer.') +        if index < -len(iobject) or index >= len(iobject): +            raise InterpreterException('Index %d out of bounds of array of size %d.' % (index, len(iobject))) +        return iobject[index] + +    def function_call(self, node): +        func_name = node.func_name +        (posargs, kwargs) = self.reduce_arguments(node.args) +        if func_name in self.funcs: +            return self.funcs[func_name](node, self.flatten(posargs), kwargs) +        else: +            self.unknown_function_called(func_name) + +    def method_call(self, node): +        invokable = node.source_object +        if isinstance(invokable, mparser.IdNode): +            object_name = invokable.value +            obj = self.get_variable(object_name) +        else: +            obj = self.evaluate_statement(invokable) +        method_name = node.name +        args = node.args +        if isinstance(obj, mparser.StringNode): +            obj = obj.get_value() +        if isinstance(obj, str): +            return self.string_method_call(obj, method_name, args) +        if isinstance(obj, bool): +            return self.bool_method_call(obj, method_name, args) +        if isinstance(obj, int): +            return self.int_method_call(obj, method_name, args) +        if isinstance(obj, list): +            return self.array_method_call(obj, method_name, self.reduce_arguments(args)[0]) +        if not isinstance(obj, InterpreterObject): +            raise InvalidArguments('Variable "%s" is not callable.' % object_name) +        (args, kwargs) = self.reduce_arguments(args) +        if method_name == 'extract_objects': +            self.validate_extraction(obj.held_object) +        return obj.method_call(method_name, self.flatten(args), kwargs) + +    def bool_method_call(self, obj, method_name, args): +        obj = self.to_native(obj) +        (posargs, _) = self.reduce_arguments(args) +        if method_name == 'to_string': +            if len(posargs) == 0: +                if obj == True: +                    return 'true' +                else: +                    return 'false' +            elif len(posargs) == 2 and isinstance(posargs[0], str) and isinstance(posargs[1], str): +                if obj == True: +                    return posargs[0] +                else: +                    return posargs[1] +            else: +                raise InterpreterException('bool.to_string() must have either no arguments or exactly two string arguments that signify what values to return for true and false.') +        elif method_name == 'to_int': +            if obj == True: +                return 1 +            else: +                return 0 +        else: +            raise InterpreterException('Unknown method "%s" for a boolean.' % method_name) + +    def int_method_call(self, obj, method_name, args): +        obj = self.to_native(obj) +        (posargs, _) = self.reduce_arguments(args) +        if method_name == 'is_even': +            if len(posargs) == 0: +                return obj % 2 == 0 +            else: +                raise InterpreterException('int.is_even() must have no arguments.') +        elif method_name == 'is_odd': +            if len(posargs) == 0: +                return obj % 2 != 0 +            else: +                raise InterpreterException('int.is_odd() must have no arguments.') +        else: +            raise InterpreterException('Unknown method "%s" for an integer.' % method_name) + +    def string_method_call(self, obj, method_name, args): +        obj = self.to_native(obj) +        (posargs, _) = self.reduce_arguments(args) +        if method_name == 'strip': +            return obj.strip() +        elif method_name == 'format': +            return self.format_string(obj, args) +        elif method_name == 'to_upper': +            return obj.upper() +        elif method_name == 'to_lower': +            return obj.lower() +        elif method_name == 'underscorify': +            return re.sub(r'[^a-zA-Z0-9]', '_', obj) +        elif method_name == 'split': +            if len(posargs) > 1: +                raise InterpreterException('Split() must have at most one argument.') +            elif len(posargs) == 1: +                s = posargs[0] +                if not isinstance(s, str): +                    raise InterpreterException('Split() argument must be a string') +                return obj.split(s) +            else: +                return obj.split() +        elif method_name == 'startswith' or method_name == 'contains' or method_name == 'endswith': +            s = posargs[0] +            if not isinstance(s, str): +                raise InterpreterException('Argument must be a string.') +            if method_name == 'startswith': +                return obj.startswith(s) +            elif method_name == 'contains': +                return obj.find(s) >= 0 +            return obj.endswith(s) +        elif method_name == 'to_int': +            try: +                return int(obj) +            except Exception: +                raise InterpreterException('String {!r} cannot be converted to int'.format(obj)) +        elif method_name == 'join': +            if len(posargs) != 1: +                raise InterpreterException('Join() takes exactly one argument.') +            strlist = posargs[0] +            check_stringlist(strlist) +            return obj.join(strlist) +        elif method_name == 'version_compare': +            if len(posargs) != 1: +                raise InterpreterException('Version_compare() takes exactly one argument.') +            cmpr = posargs[0] +            if not isinstance(cmpr, str): +                raise InterpreterException('Version_compare() argument must be a string.') +            return mesonlib.version_compare(obj, cmpr) +        raise InterpreterException('Unknown method "%s" for a string.' % method_name) + +    def unknown_function_called(self, func_name): +            raise InvalidCode('Unknown function "%s".' % func_name) + +    def array_method_call(self, obj, method_name, args): +        if method_name == 'contains': +            return self.check_contains(obj, args) +        elif method_name == 'length': +            return len(obj) +        elif method_name == 'get': +            index = args[0] +            if not isinstance(index, int): +                raise InvalidArguments('Array index must be a number.') +            if index < -len(obj) or index >= len(obj): +                raise InvalidArguments('Array index %s is out of bounds for array of size %d.' % (index, len(obj))) +            return obj[index] +        raise InterpreterException('Arrays do not have a method called "%s".' % method_name) + + +    def reduce_arguments(self, args): +        assert(isinstance(args, mparser.ArgumentNode)) +        if args.incorrect_order(): +            raise InvalidArguments('All keyword arguments must be after positional arguments.') +        reduced_pos = [self.evaluate_statement(arg) for arg in args.arguments] +        reduced_kw = {} +        for key in args.kwargs.keys(): +            if not isinstance(key, str): +                raise InvalidArguments('Keyword argument name is not a string.') +            a = args.kwargs[key] +            reduced_kw[key] = self.evaluate_statement(a) +        if not isinstance(reduced_pos, list): +            reduced_pos = [reduced_pos] +        return (reduced_pos, reduced_kw) + +    def flatten(self, args): +        if isinstance(args, mparser.StringNode): +            return args.value +        if isinstance(args, (int, str, InterpreterObject)): +            return args +        result = [] +        for a in args: +            if isinstance(a, list): +                rest = self.flatten(a) +                result = result + rest +            elif isinstance(a, mparser.StringNode): +                result.append(a.value) +            else: +                result.append(a) +        return result + +    def assignment(self, node): +        assert(isinstance(node, mparser.AssignmentNode)) +        var_name = node.var_name +        if not isinstance(var_name, str): +            raise InvalidArguments('Tried to assign value to a non-variable.') +        value = self.evaluate_statement(node.value) +        value = self.to_native(value) +        if not self.is_assignable(value): +            raise InvalidCode('Tried to assign an invalid value to variable.') +        # For mutable objects we need to make a copy on assignment +        if isinstance(value, MutableInterpreterObject): +            value = copy.deepcopy(value) +        self.set_variable(var_name, value) +        return value + +    def set_variable(self, varname, variable): +        if variable is None: +            raise InvalidCode('Can not assign None to variable.') +        if not isinstance(varname, str): +            raise InvalidCode('First argument to set_variable must be a string.') +        if not self.is_assignable(variable): +            raise InvalidCode('Assigned value not of assignable type.') +        if re.match('[_a-zA-Z][_0-9a-zA-Z]*$', varname) is None: +            raise InvalidCode('Invalid variable name: ' + varname) +        if varname in self.builtin: +            raise InvalidCode('Tried to overwrite internal variable "%s"' % varname) +        self.variables[varname] = variable + +    def get_variable(self, varname): +        if varname in self.builtin: +            return self.builtin[varname] +        if varname in self.variables: +            return self.variables[varname] +        raise InvalidCode('Unknown variable "%s".' % varname) + +    def to_native(self, arg): +        if isinstance(arg, (mparser.StringNode, mparser.NumberNode, +                            mparser.BooleanNode)): +            return arg.value +        return arg + +    def is_assignable(self, value): +        return isinstance(value, (InterpreterObject, dependencies.Dependency, +                                  str, int, list, mesonlib.File)) + +    def func_build_target(self, node, args, kwargs): +        if 'target_type' not in kwargs: +            raise InterpreterException('Missing target_type keyword argument') +        target_type = kwargs.pop('target_type') +        if target_type == 'executable': +            return self.func_executable(node, args, kwargs) +        elif target_type == 'shared_library': +            return self.func_shared_lib(node, args, kwargs) +        elif target_type == 'static_library': +            return self.func_static_lib(node, args, kwargs) +        elif target_type == 'library': +            return self.func_library(node, args, kwargs) +        elif target_type == 'jar': +            return self.func_jar(node, args, kwargs) +        else: +            raise InterpreterException('Unknown target_type.') + +    def func_set_variable(self, node, args, kwargs): +        if len(args) != 2: +            raise InvalidCode('Set_variable takes two arguments.') +        varname = args[0] +        value = self.to_native(args[1]) +        self.set_variable(varname, value) + +#    @noKwargs +    def func_get_variable(self, node, args, kwargs): +        if len(args)<1 or len(args)>2: +            raise InvalidCode('Get_variable takes one or two arguments.') +        varname = args[0] +        if not isinstance(varname, str): +            raise InterpreterException('First argument must be a string.') +        try: +            return self.variables[varname] +        except KeyError: +            pass +        if len(args) == 2: +            return args[1] +        raise InterpreterException('Tried to get unknown variable "%s".' % varname) + +    @stringArgs +    @noKwargs +    def func_is_variable(self, node, args, kwargs): +        if len(args) != 1: +            raise InvalidCode('Is_variable takes two arguments.') +        varname = args[0] +        return varname in self.variables + +    def is_elementary_type(self, v): +        return isinstance(v, (int, float, str, bool, list)) + diff --git a/mesonbuild/mparser.py b/mesonbuild/mparser.py index f593c8e31..ad1feddae 100644 --- a/mesonbuild/mparser.py +++ b/mesonbuild/mparser.py @@ -22,10 +22,12 @@ class ParseException(MesonException):          self.colno = colno  class Token: -    def __init__(self, tid, lineno, colno, value): +    def __init__(self, tid, subdir, lineno, colno, bytespan, value):          self.tid = tid +        self.subdir = subdir          self.lineno = lineno          self.colno = colno +        self.bytespan = bytespan          self.value = value      def __eq__(self, other): @@ -71,7 +73,7 @@ class Lexer:              ('questionmark', re.compile(r'\?')),          ] -    def lex(self, code): +    def lex(self, code, subdir):          lineno = 1          line_start = 0          loc = 0; @@ -87,7 +89,10 @@ class Lexer:                      curline = lineno                      col = mo.start()-line_start                      matched = True +                    span_start = loc                      loc = mo.end() +                    span_end = loc +                    bytespan = (span_start, span_end)                      match_text = mo.group()                      if tid == 'ignore' or tid == 'comment':                          break @@ -123,40 +128,41 @@ class Lexer:                              tid = match_text                          else:                              value = match_text -                    yield Token(tid, curline, col, value) +                    yield Token(tid, subdir, curline, col, bytespan, value)                      break              if not matched:                  raise ParseException('lexer', lineno, col) -class BooleanNode: -    def __init__(self, token, value): +class ElementaryNode: +    def __init__(self, token):          self.lineno = token.lineno +        self.subdir = token.subdir          self.colno = token.colno +        self.value = token.value +        self.bytespan = token.bytespan + +class BooleanNode(ElementaryNode): +    def __init__(self, token, value): +        super().__init__(token)          self.value = value          assert(isinstance(self.value, bool)) -class IdNode: +class IdNode(ElementaryNode):      def __init__(self, token): -        self.lineno = token.lineno -        self.colno = token.colno -        self.value = token.value +        super().__init__(token)          assert(isinstance(self.value, str))      def __str__(self):          return "Id node: '%s' (%d, %d)." % (self.value, self.lineno, self.colno) -class NumberNode: +class NumberNode(ElementaryNode):      def __init__(self, token): -        self.lineno = token.lineno -        self.colno = token.colno -        self.value = token.value +        super().__init__(token)          assert(isinstance(self.value, int)) -class StringNode: +class StringNode(ElementaryNode):      def __init__(self, token): -        self.lineno = token.lineno -        self.colno = token.colno -        self.value = token.value +        super().__init__(token)          assert(isinstance(self.value, str))      def __str__(self): @@ -164,20 +170,23 @@ class StringNode:  class ArrayNode:      def __init__(self, args): +        self.subdir = args.subdir          self.lineno = args.lineno          self.colno = args.colno          self.args = args  class EmptyNode:      def __init__(self): +        self.subdir =''          self.lineno = 0          self.colno = 0          self.value = None  class OrNode: -    def __init__(self, lineno, colno, left, right): -        self.lineno = lineno -        self.colno = colno +    def __init__(self, left, right): +        self.subdir = left.subdir +        self.lineno = left.lineno +        self.colno = left.colno          self.left = left          self.right = right @@ -189,42 +198,48 @@ class AndNode:          self.right = right  class ComparisonNode: -    def __init__(self, lineno, colno, ctype, left, right): -        self.lineno = lineno -        self.colno = colno +    def __init__(self, ctype, left, right): +        self.lineno = left.lineno +        self.colno = left.colno +        self.subdir = left.subdir          self.left = left          self.right = right          self.ctype = ctype  class ArithmeticNode: -    def __init__(self, lineno, colno, operation, left, right): -        self.lineno = lineno -        self.colno = colno +    def __init__(self,operation, left, right): +        self.subdir = left.subdir +        self.lineno = left.lineno +        self.colno = left.colno          self.left = left          self.right = right          self.operation = operation  class NotNode: -    def __init__(self, lineno, colno, value): -        self.lineno = lineno -        self.colno = colno +    def __init__(self, location_node, value): +        self.subdir = location_node.subdir +        self.lineno = location_node.lineno +        self.colno = location_node.colno          self.value = value  class CodeBlockNode: -    def __init__(self, lineno, colno): -        self.lineno = lineno -        self.colno = colno +    def __init__(self, location_node): +        self.subdir = location_node.subdir +        self.lineno = location_node.lineno +        self.colno = location_node.colno          self.lines = []  class IndexNode:      def __init__(self, iobject, index):          self.iobject = iobject          self.index = index +        self.subdir = iobject.subdir          self.lineno = iobject.lineno          self.colno = iobject.colno  class MethodNode: -    def __init__(self, lineno, colno, source_object, name, args): +    def __init__(self, subdir, lineno, colno, source_object, name, args): +        self.subdir = subdir          self.lineno = lineno          self.colno = colno          self.source_object = source_object @@ -233,7 +248,8 @@ class MethodNode:          self.args = args  class FunctionNode: -    def __init__(self, lineno, colno, func_name, args): +    def __init__(self, subdir, lineno, colno, func_name, args): +        self.subdir = subdir          self.lineno = lineno          self.colno = colno          self.func_name = func_name @@ -272,9 +288,10 @@ class IfClauseNode():          self.elseblock = EmptyNode()  class UMinusNode(): -    def __init__(self, lineno, colno, value): -        self.lineno = lineno -        self.colno = colno +    def __init__(self, current_location, value): +        self.subdir = current_location.subdir +        self.lineno = current_location.lineno +        self.colno = current_location.colno          self.value = value  class IfNode(): @@ -296,7 +313,9 @@ class ArgumentNode():      def __init__(self, token):          self.lineno = token.lineno          self.colno = token.colno +        self.subdir = token.subdir          self.arguments = [] +        self.commas = []          self.kwargs = {}          self.order_error = False @@ -351,8 +370,8 @@ comparison_map = {'equal': '==',  # 9 plain token  class Parser: -    def __init__(self, code): -        self.stream = Lexer().lex(code) +    def __init__(self, code, subdir): +        self.stream = Lexer().lex(code, subdir)          self.getsym()          self.in_ternary = False @@ -360,7 +379,7 @@ class Parser:          try:              self.current = next(self.stream)          except StopIteration: -            self.current = Token('eof', 0, 0, None) +            self.current = Token('eof', '', 0, 0, (0, 0), None)      def accept(self, s):          if self.current.tid == s: @@ -409,7 +428,7 @@ class Parser:      def e2(self):          left = self.e3()          while self.accept('or'): -            left = OrNode(left.lineno, left.colno, left, self.e3()) +            left = OrNode(left, self.e3())          return left      def e3(self): @@ -422,7 +441,7 @@ class Parser:          left = self.e5()          for nodename, operator_type in comparison_map.items():              if self.accept(nodename): -                return ComparisonNode(left.lineno, left.colno, operator_type, left, self.e5()) +                return ComparisonNode(operator_type, left, self.e5())          return left      def e5(self): @@ -431,38 +450,38 @@ class Parser:      def e5add(self):          left = self.e5sub()          if self.accept('plus'): -            return ArithmeticNode(left.lineno, left.colno, 'add', left, self.e5add()) +            return ArithmeticNode('add', left, self.e5add())          return left      def e5sub(self):          left = self.e5mod()          if self.accept('dash'): -            return ArithmeticNode(left.lineno, left.colno, 'sub', left, self.e5sub()) +            return ArithmeticNode('sub', left, self.e5sub())          return left      def e5mod(self):          left = self.e5mul()          if self.accept('percent'): -            return ArithmeticNode(left.lineno, left.colno, 'mod', left, self.e5mod()) +            return ArithmeticNode('mod', left, self.e5mod())          return left      def e5mul(self):          left = self.e5div()          if self.accept('star'): -            return ArithmeticNode(left.lineno, left.colno, 'mul', left, self.e5mul()) +            return ArithmeticNode('mul', left, self.e5mul())          return left      def e5div(self):          left = self.e6()          if self.accept('fslash'): -            return ArithmeticNode(left.lineno, left.colno, 'div', left, self.e5div()) +            return ArithmeticNode('div', left, self.e5div())          return left      def e6(self):          if self.accept('not'): -            return NotNode(self.current.lineno, self.current.colno, self.e7()) +            return NotNode(self.current, self.e7())          if self.accept('dash'): -            return UMinusNode(self.current.lineno, self.current.colno, self.e7()) +            return UMinusNode(self.current, self.e7())          return self.e7()      def e7(self): @@ -473,7 +492,7 @@ class Parser:              if not isinstance(left, IdNode):                  raise ParseException('Function call must be applied to plain id',                                       left.lineno, left.colno) -            left = FunctionNode(left.lineno, left.colno, left.value, args) +            left = FunctionNode(left.subdir, left.lineno, left.colno, left.value, args)          go_again = True          while go_again:              go_again = False @@ -516,15 +535,19 @@ class Parser:          a = ArgumentNode(s)          while not isinstance(s, EmptyNode): +            potential = self.current              if self.accept('comma'): +                a.commas.append(potential)                  a.append(s)              elif self.accept('colon'):                  if not isinstance(s, IdNode):                      raise ParseException('Keyword argument must be a plain identifier.',                                           s.lineno, s.colno)                  a.set_kwarg(s.value, self.statement()) +                potential = self.current                  if not self.accept('comma'):                      return a +                a.commas.append(potential)              else:                  a.append(s)                  return a @@ -539,7 +562,7 @@ class Parser:          self.expect('lparen')          args = self.args()          self.expect('rparen') -        method = MethodNode(methodname.lineno, methodname.colno, source_object, methodname.value, args) +        method = MethodNode(methodname.subdir, methodname.lineno, methodname.colno, source_object, methodname.value, args)          if self.accept('dot'):              return self.method_call(method)          return method @@ -593,7 +616,7 @@ class Parser:          return self.statement()      def codeblock(self): -        block = CodeBlockNode(self.current.lineno, self.current.colno) +        block = CodeBlockNode(self.current)          cond = True          while cond:              curline = self.line() diff --git a/mesonbuild/optinterpreter.py b/mesonbuild/optinterpreter.py index 9f57fd691..4fe0843dc 100644 --- a/mesonbuild/optinterpreter.py +++ b/mesonbuild/optinterpreter.py @@ -79,7 +79,7 @@ class OptionInterpreter:      def process(self, option_file):          try:              with open(option_file, 'r', encoding='utf8') as f: -                ast = mparser.Parser(f.read()).parse() +                ast = mparser.Parser(f.read(), '').parse()          except mesonlib.MesonException as me:              me.file = option_file              raise me diff --git a/mesonrewriter.py b/mesonrewriter.py new file mode 100755 index 000000000..fb857458c --- /dev/null +++ b/mesonrewriter.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# Copyright 2016 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +#     http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This class contains the basic functionality needed to run any interpreter +# or an interpreter-based tool. + +# This tool is used to manipulate an existing Meson build definition. +# +# - add a file to a target +# - remove files from a target +# - move targets +# - reindent? + +import mesonbuild.astinterpreter +from mesonbuild.mesonlib import MesonException +from mesonbuild import mlog +import sys, traceback +import argparse + +parser = argparse.ArgumentParser() + +parser.add_argument('--sourcedir', default='.', +                    help='Path to source directory.') +parser.add_argument('--target', default=None, +                    help='Name of target to edit.') +parser.add_argument('--filename', default=None, +                    help='Name of source file to add or remove to target.') +parser.add_argument('commands', nargs='+') + +if __name__ == '__main__': +    options = parser.parse_args() +    if options.target is None or options.filename is None: +        sys.exit("Must specify both target and filename.") +    print('This tool is highly experimental, use with care.') +    rewriter = mesonbuild.astinterpreter.AstInterpreter(options.sourcedir, '') +    try: +        if options.commands[0] == 'add': +            rewriter.add_source(options.target, options.filename) +        elif options.commands[0] == 'remove': +            rewriter.remove_source(options.target, options.filename) +        else: +            sys.exit('Unknown command: ' + options.commands[0]) +    except Exception as e: +        if isinstance(e, MesonException): +            if hasattr(e, 'file') and hasattr(e, 'lineno') and hasattr(e, 'colno'): +                mlog.log(mlog.red('\nMeson encountered an error in file %s, line %d, column %d:' % (e.file, e.lineno, e.colno))) +            else: +                mlog.log(mlog.red('\nMeson encountered an error:')) +            mlog.log(e) +        else: +            traceback.print_exc() +        sys.exit(1) diff --git a/run_unittests.py b/run_unittests.py index 6078b7d1b..36d899a90 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -341,5 +341,63 @@ class LinuxlikeTests(unittest.TestCase):              Oargs = [arg for arg in cmd if arg.startswith('-O')]              self.assertEqual(Oargs, [Oflag, '-O0']) +class RewriterTests(unittest.TestCase): + +    def setUp(self): +        super().setUp() +        src_root = os.path.dirname(__file__) +        self.testroot = tempfile.mkdtemp() +        self.rewrite_command = [sys.executable, os.path.join(src_root, 'mesonrewriter.py')] +        self.tmpdir = tempfile.mkdtemp() +        self.workdir = os.path.join(self.tmpdir, 'foo') +        self.test_dir = os.path.join(src_root, 'test cases/rewrite') + +    def tearDown(self): +        shutil.rmtree(self.tmpdir) + +    def read_contents(self, fname): +        with open(os.path.join(self.workdir, fname)) as f: +            return f.read() + +    def check_effectively_same(self, mainfile, truth): +        mf = self.read_contents(mainfile) +        t = self.read_contents(truth) +        # Rewriting is not guaranteed to do a perfect job of +        # maintaining whitespace. +        self.assertEqual(mf.replace(' ', ''), t.replace(' ', '')) + +    def prime(self, dirname): +        shutil.copytree(os.path.join(self.test_dir, dirname), self.workdir) + +    def test_basic(self): +        self.prime('1 basic') +        subprocess.check_output(self.rewrite_command + ['remove', +                                                        '--target=trivialprog', +                                                        '--filename=notthere.c', +                                                        '--sourcedir', self.workdir]) +        self.check_effectively_same('meson.build', 'removed.txt') +        subprocess.check_output(self.rewrite_command + ['add', +                                                        '--target=trivialprog', +                                                        '--filename=notthere.c', +                                                        '--sourcedir', self.workdir]) +        self.check_effectively_same('meson.build', 'added.txt') +        subprocess.check_output(self.rewrite_command + ['remove', +                                                        '--target=trivialprog', +                                                        '--filename=notthere.c', +                                                        '--sourcedir', self.workdir]) +        self.check_effectively_same('meson.build', 'removed.txt') + +    def test_subdir(self): +        self.prime('2 subdirs') +        top = self.read_contents('meson.build') +        s2 = self.read_contents('sub2/meson.build') +        subprocess.check_output(self.rewrite_command + ['remove', +                                                        '--target=something', +                                                        '--filename=second.c', +                                                        '--sourcedir', self.workdir]) +        self.check_effectively_same('sub1/meson.build', 'sub1/after.txt') +        self.assertEqual(top, self.read_contents('meson.build')) +        self.assertEqual(s2, self.read_contents('sub2/meson.build')) +  if __name__ == '__main__':      unittest.main() diff --git a/test cases/rewrite/1 basic/added.txt b/test cases/rewrite/1 basic/added.txt new file mode 100644 index 000000000..657dd42fb --- /dev/null +++ b/test cases/rewrite/1 basic/added.txt @@ -0,0 +1,5 @@ +project('rewritetest', 'c') + +sources = ['trivial.c'] + +exe = executable('trivialprog', 'notthere.c', sources) diff --git a/test cases/rewrite/1 basic/meson.build b/test cases/rewrite/1 basic/meson.build new file mode 100644 index 000000000..a0485d079 --- /dev/null +++ b/test cases/rewrite/1 basic/meson.build @@ -0,0 +1,5 @@ +project('rewritetest', 'c') + +sources = ['trivial.c', 'notthere.c'] + +exe = executable('trivialprog', sources) diff --git a/test cases/rewrite/1 basic/removed.txt b/test cases/rewrite/1 basic/removed.txt new file mode 100644 index 000000000..55192149c --- /dev/null +++ b/test cases/rewrite/1 basic/removed.txt @@ -0,0 +1,5 @@ +project('rewritetest', 'c') + +sources = ['trivial.c'] + +exe = executable('trivialprog', sources) diff --git a/test cases/rewrite/2 subdirs/meson.build b/test cases/rewrite/2 subdirs/meson.build new file mode 100644 index 000000000..79b7ad738 --- /dev/null +++ b/test cases/rewrite/2 subdirs/meson.build @@ -0,0 +1,5 @@ +project('subdir rewrite', 'c') + +subdir('sub1') +subdir('sub2') + diff --git a/test cases/rewrite/2 subdirs/sub1/after.txt b/test cases/rewrite/2 subdirs/sub1/after.txt new file mode 100644 index 000000000..53ceaffd3 --- /dev/null +++ b/test cases/rewrite/2 subdirs/sub1/after.txt @@ -0,0 +1 @@ +srcs = ['first.c'] diff --git a/test cases/rewrite/2 subdirs/sub1/meson.build b/test cases/rewrite/2 subdirs/sub1/meson.build new file mode 100644 index 000000000..ca4220544 --- /dev/null +++ b/test cases/rewrite/2 subdirs/sub1/meson.build @@ -0,0 +1 @@ +srcs = ['first.c', 'second.c'] diff --git a/test cases/rewrite/2 subdirs/sub2/meson.build b/test cases/rewrite/2 subdirs/sub2/meson.build new file mode 100644 index 000000000..0d92e7f42 --- /dev/null +++ b/test cases/rewrite/2 subdirs/sub2/meson.build @@ -0,0 +1,2 @@ +executable('something', srcs) +  | 
