summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJussi Pakkanen <jpakkane@gmail.com>2019-01-27 20:46:29 +0200
committerGitHub <noreply@github.com>2019-01-27 20:46:29 +0200
commit733f9a77652ecb322fff28e46ef11762241bdb09 (patch)
tree5f515fe6a44eb19d14401d90672e627063b78456
parent2dcb6eb0b3d7574b9fa97f962b715bebe1d043ef (diff)
parentdbb94f122ddeb5a37ff2603acbcc701b996958bb (diff)
downloadmeson-733f9a77652ecb322fff28e46ef11762241bdb09.tar.gz
Merge pull request #4814 from mensinda/astVisitor
rewriter: Rewrote the meson rewriter - now works with AST modification
-rw-r--r--mesonbuild/ast/__init__.py32
-rw-r--r--mesonbuild/ast/interpreter.py (renamed from mesonbuild/astinterpreter.py)226
-rw-r--r--mesonbuild/ast/introspection.py241
-rw-r--r--mesonbuild/ast/postprocess.py86
-rw-r--r--mesonbuild/ast/printer.py203
-rw-r--r--mesonbuild/ast/visitor.py140
-rw-r--r--mesonbuild/mintro.py123
-rw-r--r--mesonbuild/mparser.py66
-rw-r--r--mesonbuild/rewriter.py309
-rwxr-xr-xrun_unittests.py154
-rw-r--r--setup.py1
-rw-r--r--test cases/rewrite/1 basic/addSrc.json89
-rw-r--r--test cases/rewrite/1 basic/added.txt5
-rw-r--r--test cases/rewrite/1 basic/info.json47
-rw-r--r--test cases/rewrite/1 basic/meson.build19
-rw-r--r--test cases/rewrite/1 basic/removed.txt5
-rw-r--r--test cases/rewrite/1 basic/rmSrc.json83
-rw-r--r--test cases/rewrite/2 subdirs/addSrc.json13
-rw-r--r--test cases/rewrite/2 subdirs/info.json7
-rw-r--r--test cases/rewrite/2 subdirs/meson.build1
-rw-r--r--test cases/rewrite/2 subdirs/sub1/after.txt1
-rw-r--r--test cases/rewrite/2 subdirs/sub2/meson.build1
22 files changed, 1459 insertions, 393 deletions
diff --git a/mesonbuild/ast/__init__.py b/mesonbuild/ast/__init__.py
new file mode 100644
index 000000000..a9370dc6e
--- /dev/null
+++ b/mesonbuild/ast/__init__.py
@@ -0,0 +1,32 @@
+# Copyright 2019 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.
+
+__all__ = [
+ 'AstInterpreter',
+ 'AstIDGenerator',
+ 'AstIndentationGenerator',
+ 'AstVisitor',
+ 'AstPrinter',
+ 'IntrospectionInterpreter',
+ 'build_target_functions',
+]
+
+from .interpreter import AstInterpreter
+from .introspection import IntrospectionInterpreter, build_target_functions
+from .visitor import AstVisitor
+from .postprocess import AstIDGenerator, AstIndentationGenerator
+from .printer import AstPrinter
diff --git a/mesonbuild/astinterpreter.py b/mesonbuild/ast/interpreter.py
index f68aa7a14..20714328b 100644
--- a/mesonbuild/astinterpreter.py
+++ b/mesonbuild/ast/interpreter.py
@@ -15,10 +15,10 @@
# This class contains the basic functionality needed to run any interpreter
# or an interpreter-based tool.
-from . import interpreterbase, mparser, mesonlib
-from . import environment
+from .. import interpreterbase, mparser, mesonlib
+from .. import environment
-from .interpreterbase import InterpreterException, InvalidArguments, BreakRequest, ContinueRequest
+from ..interpreterbase import InvalidArguments, BreakRequest, ContinueRequest
import os, sys
@@ -46,6 +46,8 @@ REMOVE_SOURCE = 1
class AstInterpreter(interpreterbase.InterpreterBase):
def __init__(self, source_root, subdir):
super().__init__(source_root, subdir)
+ self.visited_subdirs = {}
+ self.assignments = {}
self.funcs.update({'project': self.func_do_nothing,
'test': self.func_do_nothing,
'benchmark': self.func_do_nothing,
@@ -83,7 +85,7 @@ class AstInterpreter(interpreterbase.InterpreterBase):
'build_target': self.func_do_nothing,
'custom_target': self.func_do_nothing,
'run_target': self.func_do_nothing,
- 'subdir': self.func_do_nothing,
+ 'subdir': self.func_subdir,
'set_variable': self.func_do_nothing,
'get_variable': self.func_do_nothing,
'is_variable': self.func_do_nothing,
@@ -92,6 +94,39 @@ class AstInterpreter(interpreterbase.InterpreterBase):
def func_do_nothing(self, node, args, kwargs):
return True
+ def func_subdir(self, node, args, kwargs):
+ args = self.flatten_args(args)
+ if len(args) != 1 or not isinstance(args[0], str):
+ sys.stderr.write('Unable to evaluate subdir({}) in AstInterpreter --> Skipping\n'.format(args))
+ return
+
+ prev_subdir = self.subdir
+ subdir = os.path.join(prev_subdir, args[0])
+ absdir = os.path.join(self.source_root, subdir)
+ buildfilename = os.path.join(self.subdir, environment.build_filename)
+ absname = os.path.join(self.source_root, buildfilename)
+ symlinkless_dir = os.path.realpath(absdir)
+ if symlinkless_dir in self.visited_subdirs:
+ sys.stderr.write('Trying to enter {} which has already been visited --> Skipping\n'.format(args[0]))
+ return
+ self.visited_subdirs[symlinkless_dir] = True
+
+ if not os.path.isfile(absname):
+ sys.stderr.write('Unable to find build file {} --> Skipping\n'.format(buildfilename))
+ return
+ with open(absname, encoding='utf8') as f:
+ code = f.read()
+ assert(isinstance(code, str))
+ try:
+ codeblock = mparser.Parser(code, self.subdir).parse()
+ except mesonlib.MesonException as me:
+ me.file = buildfilename
+ raise me
+
+ self.subdir = subdir
+ self.evaluate_codeblock(codeblock)
+ self.subdir = prev_subdir
+
def method_call(self, node):
return True
@@ -99,7 +134,11 @@ class AstInterpreter(interpreterbase.InterpreterBase):
return 0
def evaluate_plusassign(self, node):
- return 0
+ assert(isinstance(node, mparser.PlusAssignmentNode))
+ if node.var_name not in self.assignments:
+ self.assignments[node.var_name] = []
+ self.assignments[node.var_name] += [node.value] # Save a reference to the value node
+ self.evaluate_statement(node.value) # Evaluate the value just in case
def evaluate_indexing(self, node):
return 0
@@ -134,148 +173,37 @@ class AstInterpreter(interpreterbase.InterpreterBase):
return 0
def assignment(self, node):
- pass
-
-class RewriterInterpreter(AstInterpreter):
- def __init__(self, source_root, subdir):
- super().__init__(source_root, subdir)
- self.asts = {}
- self.funcs.update({'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_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)
+ assert(isinstance(node, mparser.AssignmentNode))
+ self.assignments[node.var_name] = [node.value] # Save a reference to the value node
+ self.evaluate_statement(node.value) # Evaluate the value just in case
+
+ def flatten_args(self, args, include_unknown_args: bool = False):
+ # Resolve mparser.ArrayNode if needed
+ flattend_args = []
+ temp_args = []
+ if isinstance(args, mparser.ArrayNode):
+ args = [x for x in args.args.arguments]
+ elif isinstance(args, mparser.ArgumentNode):
+ args = [x for x in args.arguments]
+ for i in args:
+ if isinstance(i, mparser.ArrayNode):
+ temp_args += [x for x in i.args.arguments]
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('Nonexistent 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 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 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]))
+ temp_args += [i]
+ for i in temp_args:
+ if isinstance(i, mparser.ElementaryNode) and not isinstance(i, mparser.IdNode):
+ flattend_args += [i.value]
+ elif isinstance(i, (str, bool, int, float)) or include_unknown_args:
+ flattend_args += [i]
+ return flattend_args
+
+ def flatten_kwargs(self, kwargs: object, include_unknown_args: bool = False):
+ flattend_kwargs = {}
+ for key, val in kwargs.items():
+ if isinstance(val, mparser.ElementaryNode):
+ flattend_kwargs[key] = val.value
+ elif isinstance(val, (mparser.ArrayNode, mparser.ArgumentNode)):
+ flattend_kwargs[key] = self.flatten_args(val, include_unknown_args)
+ elif isinstance(val, (str, bool, int, float)) or include_unknown_args:
+ flattend_kwargs[key] = val
+ return flattend_kwargs
diff --git a/mesonbuild/ast/introspection.py b/mesonbuild/ast/introspection.py
new file mode 100644
index 000000000..5d0ec5aca
--- /dev/null
+++ b/mesonbuild/ast/introspection.py
@@ -0,0 +1,241 @@
+# Copyright 2018 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 AstInterpreter
+from .. import compilers, environment, mesonlib, mparser, optinterpreter
+from .. import coredata as cdata
+from ..interpreterbase import InvalidArguments
+from ..build import Executable, Jar, SharedLibrary, SharedModule, StaticLibrary
+import os
+
+build_target_functions = ['executable', 'jar', 'library', 'shared_library', 'shared_module', 'static_library', 'both_libraries']
+
+class IntrospectionHelper:
+ # mimic an argparse namespace
+ def __init__(self, cross_file):
+ self.cross_file = cross_file
+ self.native_file = None
+ self.cmd_line_options = {}
+
+class IntrospectionInterpreter(AstInterpreter):
+ # Interpreter to detect the options without a build directory
+ # Most of the code is stolen from interperter.Interpreter
+ def __init__(self, source_root, subdir, backend, cross_file=None, subproject='', subproject_dir='subprojects', env=None):
+ super().__init__(source_root, subdir)
+
+ options = IntrospectionHelper(cross_file)
+ self.cross_file = cross_file
+ if env is None:
+ self.environment = environment.Environment(source_root, None, options)
+ else:
+ self.environment = env
+ self.subproject = subproject
+ self.subproject_dir = subproject_dir
+ self.coredata = self.environment.get_coredata()
+ self.option_file = os.path.join(self.source_root, self.subdir, 'meson_options.txt')
+ self.backend = backend
+ self.default_options = {'backend': self.backend}
+ self.project_data = {}
+ self.targets = []
+
+ self.funcs.update({
+ 'add_languages': self.func_add_languages,
+ 'executable': self.func_executable,
+ 'jar': self.func_jar,
+ 'library': self.func_library,
+ 'project': self.func_project,
+ 'shared_library': self.func_shared_lib,
+ 'shared_module': self.func_shared_module,
+ 'static_library': self.func_static_lib,
+ 'both_libraries': self.func_both_lib,
+ })
+
+ def func_project(self, node, args, kwargs):
+ if len(args) < 1:
+ raise InvalidArguments('Not enough arguments to project(). Needs at least the project name.')
+
+ proj_name = args[0]
+ proj_vers = kwargs.get('version', 'undefined')
+ proj_langs = self.flatten_args(args[1:])
+ if isinstance(proj_vers, mparser.ElementaryNode):
+ proj_vers = proj_vers.value
+ if not isinstance(proj_vers, str):
+ proj_vers = 'undefined'
+ self.project_data = {'descriptive_name': proj_name, 'version': proj_vers}
+
+ if os.path.exists(self.option_file):
+ oi = optinterpreter.OptionInterpreter(self.subproject)
+ oi.process(self.option_file)
+ self.coredata.merge_user_options(oi.options)
+
+ def_opts = self.flatten_args(kwargs.get('default_options', []))
+ self.project_default_options = mesonlib.stringlistify(def_opts)
+ self.project_default_options = cdata.create_options_dict(self.project_default_options)
+ self.default_options.update(self.project_default_options)
+ self.coredata.set_default_options(self.default_options, self.subproject, self.environment.cmd_line_options)
+
+ if not self.is_subproject() and 'subproject_dir' in kwargs:
+ spdirname = kwargs['subproject_dir']
+ if isinstance(spdirname, str):
+ self.subproject_dir = spdirname
+ if not self.is_subproject():
+ self.project_data['subprojects'] = []
+ subprojects_dir = os.path.join(self.source_root, self.subproject_dir)
+ if os.path.isdir(subprojects_dir):
+ for i in os.listdir(subprojects_dir):
+ if os.path.isdir(os.path.join(subprojects_dir, i)):
+ self.do_subproject(i)
+
+ self.coredata.init_backend_options(self.backend)
+ options = {k: v for k, v in self.environment.cmd_line_options.items() if k.startswith('backend_')}
+
+ self.coredata.set_options(options)
+ self.func_add_languages(None, proj_langs, None)
+
+ def do_subproject(self, dirname):
+ subproject_dir_abs = os.path.join(self.environment.get_source_dir(), self.subproject_dir)
+ subpr = os.path.join(subproject_dir_abs, dirname)
+ try:
+ subi = IntrospectionInterpreter(subpr, '', self.backend, cross_file=self.cross_file, subproject=dirname, subproject_dir=self.subproject_dir, env=self.environment)
+ subi.analyze()
+ subi.project_data['name'] = dirname
+ self.project_data['subprojects'] += [subi.project_data]
+ except:
+ return
+
+ def func_add_languages(self, node, args, kwargs):
+ args = self.flatten_args(args)
+ need_cross_compiler = self.environment.is_cross_build()
+ for lang in sorted(args, key=compilers.sort_clink):
+ lang = lang.lower()
+ if lang not in self.coredata.compilers:
+ self.environment.detect_compilers(lang, need_cross_compiler)
+
+ def build_target(self, node, args, kwargs, targetclass):
+ if not args:
+ return
+ kwargs = self.flatten_kwargs(kwargs, True)
+ name = self.flatten_args(args)[0]
+ srcqueue = [node]
+ if 'sources' in kwargs:
+ srcqueue += kwargs['sources']
+
+ source_nodes = []
+ while srcqueue:
+ curr = srcqueue.pop(0)
+ arg_node = None
+ if isinstance(curr, mparser.FunctionNode):
+ arg_node = curr.args
+ elif isinstance(curr, mparser.ArrayNode):
+ arg_node = curr.args
+ elif isinstance(curr, mparser.IdNode):
+ # Try to resolve the ID and append the node to the queue
+ id = curr.value
+ if id in self.assignments and self.assignments[id]:
+ node = self.assignments[id][0]
+ if isinstance(node, (mparser.ArrayNode, mparser.IdNode, mparser.FunctionNode)):
+ srcqueue += [node]
+ if arg_node is None:
+ continue
+ elemetary_nodes = list(filter(lambda x: isinstance(x, (str, mparser.StringNode)), arg_node.arguments))
+ srcqueue += list(filter(lambda x: isinstance(x, (mparser.FunctionNode, mparser.ArrayNode, mparser.IdNode)), arg_node.arguments))
+ # Pop the first element if the function is a build target function
+ if isinstance(curr, mparser.FunctionNode) and curr.func_name in build_target_functions:
+ elemetary_nodes.pop(0)
+ if elemetary_nodes:
+ source_nodes += [curr]
+
+ # Filter out kwargs from other target types. For example 'soversion'
+ # passed to library() when default_library == 'static'.
+ kwargs = {k: v for k, v in kwargs.items() if k in targetclass.known_kwargs}
+
+ is_cross = False
+ objects = []
+ empty_sources = [] # Passing the unresolved sources list causes errors
+ target = targetclass(name, self.subdir, self.subproject, is_cross, empty_sources, objects, self.environment, kwargs)
+
+ self.targets += [{
+ 'name': target.get_basename(),
+ 'id': target.get_id(),
+ 'type': target.get_typename(),
+ 'defined_in': os.path.normpath(os.path.join(self.source_root, self.subdir, environment.build_filename)),
+ 'subdir': self.subdir,
+ 'build_by_default': target.build_by_default,
+ 'sources': source_nodes,
+ 'kwargs': kwargs,
+ 'node': node,
+ }]
+
+ return
+
+ def build_library(self, node, args, kwargs):
+ default_library = self.coredata.get_builtin_option('default_library')
+ if default_library == 'shared':
+ return self.build_target(node, args, kwargs, SharedLibrary)
+ elif default_library == 'static':
+ return self.build_target(node, args, kwargs, StaticLibrary)
+ elif default_library == 'both':
+ return self.build_target(node, args, kwargs, SharedLibrary)
+
+ def func_executable(self, node, args, kwargs):
+ return self.build_target(node, args, kwargs, Executable)
+
+ def func_static_lib(self, node, args, kwargs):
+ return self.build_target(node, args, kwargs, StaticLibrary)
+
+ def func_shared_lib(self, node, args, kwargs):
+ return self.build_target(node, args, kwargs, SharedLibrary)
+
+ def func_both_lib(self, node, args, kwargs):
+ return self.build_target(node, args, kwargs, SharedLibrary)
+
+ def func_shared_module(self, node, args, kwargs):
+ return self.build_target(node, args, kwargs, SharedModule)
+
+ def func_library(self, node, args, kwargs):
+ return self.build_library(node, args, kwargs)
+
+ def func_jar(self, node, args, kwargs):
+ return self.build_target(node, args, kwargs, Jar)
+
+ def func_build_target(self, node, args, kwargs):
+ if 'target_type' not in kwargs:
+ return
+ target_type = kwargs.pop('target_type')
+ if isinstance(target_type, mparser.ElementaryNode):
+ target_type = target_type.value
+ if target_type == 'executable':
+ return self.build_target(node, args, kwargs, Executable)
+ elif target_type == 'shared_library':
+ return self.build_target(node, args, kwargs, SharedLibrary)
+ elif target_type == 'static_library':
+ return self.build_target(node, args, kwargs, StaticLibrary)
+ elif target_type == 'both_libraries':
+ return self.build_target(node, args, kwargs, SharedLibrary)
+ elif target_type == 'library':
+ return self.build_library(node, args, kwargs)
+ elif target_type == 'jar':
+ return self.build_target(node, args, kwargs, Jar)
+
+ def is_subproject(self):
+ return self.subproject != ''
+
+ def analyze(self):
+ self.load_root_meson_file()
+ self.sanity_check_ast()
+ self.parse_project()
+ self.run()
diff --git a/mesonbuild/ast/postprocess.py b/mesonbuild/ast/postprocess.py
new file mode 100644
index 000000000..e913b4f04
--- /dev/null
+++ b/mesonbuild/ast/postprocess.py
@@ -0,0 +1,86 @@
+# Copyright 2019 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 AstVisitor
+from .. import mparser
+
+class AstIndentationGenerator(AstVisitor):
+ def __init__(self):
+ self.level = 0
+
+ def visit_default_func(self, node: mparser.BaseNode):
+ # Store the current level in the node
+ node.level = self.level
+
+ def visit_ArrayNode(self, node: mparser.ArrayNode):
+ self.visit_default_func(node)
+ self.level += 1
+ node.args.accept(self)
+ self.level -= 1
+
+ def visit_DictNode(self, node: mparser.DictNode):
+ self.visit_default_func(node)
+ self.level += 1
+ node.args.accept(self)
+ self.level -= 1
+
+ def visit_MethodNode(self, node: mparser.MethodNode):
+ self.visit_default_func(node)
+ node.source_object.accept(self)
+ self.level += 1
+ node.args.accept(self)
+ self.level -= 1
+
+ def visit_FunctionNode(self, node: mparser.FunctionNode):
+ self.visit_default_func(node)
+ self.level += 1
+ node.args.accept(self)
+ self.level -= 1
+
+ def visit_ForeachClauseNode(self, node: mparser.ForeachClauseNode):
+ self.visit_default_func(node)
+ self.level += 1
+ node.items.accept(self)
+ node.block.accept(self)
+ self.level -= 1
+
+ def visit_IfClauseNode(self, node: mparser.IfClauseNode):
+ self.visit_default_func(node)
+ for i in node.ifs:
+ i.accept(self)
+ if node.elseblock:
+ self.level += 1
+ node.elseblock.accept(self)
+ self.level -= 1
+
+ def visit_IfNode(self, node: mparser.IfNode):
+ self.visit_default_func(node)
+ self.level += 1
+ node.condition.accept(self)
+ node.block.accept(self)
+ self.level -= 1
+
+class AstIDGenerator(AstVisitor):
+ def __init__(self):
+ self.counter = {}
+
+ def visit_default_func(self, node: mparser.BaseNode):
+ name = type(node).__name__
+ if name not in self.counter:
+ self.counter[name] = 0
+ node.ast_id = name + '#' + str(self.counter[name])
+ self.counter[name] += 1
diff --git a/mesonbuild/ast/printer.py b/mesonbuild/ast/printer.py
new file mode 100644
index 000000000..60e0b0d94
--- /dev/null
+++ b/mesonbuild/ast/printer.py
@@ -0,0 +1,203 @@
+# Copyright 2019 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
+from . import AstVisitor
+import re
+
+arithmic_map = {
+ 'add': '+',
+ 'sub': '-',
+ 'mod': '%',
+ 'mul': '*',
+ 'div': '/'
+}
+
+class AstPrinter(AstVisitor):
+ def __init__(self, indent: int = 2, arg_newline_cutoff: int = 5):
+ self.result = ''
+ self.indent = indent
+ self.arg_newline_cutoff = arg_newline_cutoff
+ self.ci = ''
+ self.is_newline = True
+ self.last_level = 0
+
+ def post_process(self):
+ self.result = re.sub(r'\s+\n', '\n', self.result)
+
+ def append(self, data: str, node: mparser.BaseNode):
+ level = 0
+ if node and hasattr(node, 'level'):
+ level = node.level
+ else:
+ level = self.last_level
+ self.last_level = level
+ if self.is_newline:
+ self.result += ' ' * (level * self.indent)
+ self.result += data
+ self.is_newline = False
+
+ def append_padded(self, data: str, node: mparser.BaseNode):
+ if self.result[-1] not in [' ', '\n']:
+ data = ' ' + data
+ self.append(data + ' ', node)
+
+ def newline(self):
+ self.result += '\n'
+ self.is_newline = True
+
+ def visit_BooleanNode(self, node: mparser.BooleanNode):
+ self.append('true' if node.value else 'false', node)
+
+ def visit_IdNode(self, node: mparser.IdNode):
+ self.append(node.value, node)
+
+ def visit_NumberNode(self, node: mparser.NumberNode):
+ self.append(str(node.value), node)
+
+ def visit_StringNode(self, node: mparser.StringNode):
+ self.append("'" + node.value + "'", node)
+
+ def visit_ContinueNode(self, node: mparser.ContinueNode):
+ self.append('continue', node)
+
+ def visit_BreakNode(self, node: mparser.BreakNode):
+ self.append('break', node)
+
+ def visit_ArrayNode(self, node: mparser.ArrayNode):
+ self.append('[', node)
+ node.args.accept(self)
+ self.append(']', node)
+
+ def visit_DictNode(self, node: mparser.DictNode):
+ self.append('{', node)
+ node.args.accept(self)
+ self.append('}', node)
+
+ def visit_OrNode(self, node: mparser.OrNode):
+ node.left.accept(self)
+ self.append_padded('or', node)
+ node.right.accept(self)
+
+ def visit_AndNode(self, node: mparser.AndNode):
+ node.left.accept(self)
+ self.append_padded('and', node)
+ node.right.accept(self)
+
+ def visit_ComparisonNode(self, node: mparser.ComparisonNode):
+ node.left.accept(self)
+ self.append_padded(mparser.comparison_map[node.ctype], node)
+ node.right.accept(self)
+
+ def visit_ArithmeticNode(self, node: mparser.ArithmeticNode):
+ node.left.accept(self)
+ self.append_padded(arithmic_map[node.operation], node)
+ node.right.accept(self)
+
+ def visit_NotNode(self, node: mparser.NotNode):
+ self.append_padded('not', node)
+ node.value.accept(self)
+
+ def visit_CodeBlockNode(self, node: mparser.CodeBlockNode):
+ for i in node.lines:
+ i.accept(self)
+ self.newline()
+
+ def visit_IndexNode(self, node: mparser.IndexNode):
+ self.append('[', node)
+ node.index.accept(self)
+ self.append(']', node)
+
+ def visit_MethodNode(self, node: mparser.MethodNode):
+ node.source_object.accept(self)
+ self.append('.' + node.name + '(', node)
+ node.args.accept(self)
+ self.append(')', node)
+
+ def visit_FunctionNode(self, node: mparser.FunctionNode):
+ self.append(node.func_name + '(', node)
+ node.args.accept(self)
+ self.append(')', node)
+
+ def visit_AssignmentNode(self, node: mparser.AssignmentNode):
+ self.append(node.var_name + ' = ', node)
+ node.value.accept(self)
+
+ def visit_PlusAssignmentNode(self, node: mparser.PlusAssignmentNode):
+ self.append(node.var_name + ' += ', node)
+ node.value.accept(self)
+
+ def visit_ForeachClauseNode(self, node: mparser.ForeachClauseNode):
+ varnames = [x.value for x in node.varnames]
+ self.append_padded('foreach', node)
+ self.append_padded(', '.join(varnames), node)
+ self.append_padded(':', node)
+ node.items.accept(self)
+ self.newline()
+ node.block.accept(self)
+ self.append('endforeach', node)
+
+ def visit_IfClauseNode(self, node: mparser.IfClauseNode):
+ prefix = ''
+ for i in node.ifs:
+ self.append_padded(prefix + 'if', node)
+ prefix = 'el'
+ i.accept(self)
+ if node.elseblock:
+ self.append('else', node)
+ node.elseblock.accept(self)
+ self.append('endif', node)
+
+ def visit_UMinusNode(self, node: mparser.UMinusNode):
+ self.append_padded('-', node)
+ node.value.accept(self)
+
+ def visit_IfNode(self, node: mparser.IfNode):
+ node.condition.accept(self)
+ self.newline()
+ node.block.accept(self)
+
+ def visit_TernaryNode(self, node: mparser.TernaryNode):
+ node.condition.accept(self)
+ self.append_padded('?', node)
+ node.trueblock.accept(self)
+ self.append_padded(':', node)
+ node.falseblock.accept(self)
+
+ def visit_ArgumentNode(self, node: mparser.ArgumentNode):
+ break_args = True if (len(node.arguments) + len(node.kwargs)) > self.arg_newline_cutoff else False
+ for i in node.arguments + list(node.kwargs.values()):
+ if not isinstance(i, mparser.ElementaryNode):
+ break_args = True
+ if break_args:
+ self.newline()
+ for i in node.arguments:
+ i.accept(self)
+ self.append(', ', node)
+ if break_args:
+ self.newline()
+ for key, val in node.kwargs.items():
+ self.append(key, node)
+ self.append_padded(':', node)
+ val.accept(self)
+ self.append(', ', node)
+ if break_args:
+ self.newline()
+ if break_args:
+ self.result = re.sub(r', \n$', '\n', self.result)
+ else:
+ self.result = re.sub(r', $', '', self.result)
diff --git a/mesonbuild/ast/visitor.py b/mesonbuild/ast/visitor.py
new file mode 100644
index 000000000..c8769d436
--- /dev/null
+++ b/mesonbuild/ast/visitor.py
@@ -0,0 +1,140 @@
+# Copyright 2019 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
+
+class AstVisitor:
+ def __init__(self):
+ pass
+
+ def visit_default_func(self, node: mparser.BaseNode):
+ pass
+
+ def visit_BooleanNode(self, node: mparser.BooleanNode):
+ self.visit_default_func(node)
+
+ def visit_IdNode(self, node: mparser.IdNode):
+ self.visit_default_func(node)
+
+ def visit_NumberNode(self, node: mparser.NumberNode):
+ self.visit_default_func(node)
+
+ def visit_StringNode(self, node: mparser.StringNode):
+ self.visit_default_func(node)
+
+ def visit_ContinueNode(self, node: mparser.ContinueNode):
+ self.visit_default_func(node)
+
+ def visit_BreakNode(self, node: mparser.BreakNode):
+ self.visit_default_func(node)
+
+ def visit_ArrayNode(self, node: mparser.ArrayNode):
+ self.visit_default_func(node)
+ node.args.accept(self)
+
+ def visit_DictNode(self, node: mparser.DictNode):
+ self.visit_default_func(node)
+ node.args.accept(self)
+
+ def visit_EmptyNode(self, node: mparser.EmptyNode):
+ self.visit_default_func(node)
+
+ def visit_OrNode(self, node: mparser.OrNode):
+ self.visit_default_func(node)
+ node.left.accept(self)
+ node.right.accept(self)
+
+ def visit_AndNode(self, node: mparser.AndNode):
+ self.visit_default_func(node)
+ node.left.accept(self)
+ node.right.accept(self)
+
+ def visit_ComparisonNode(self, node: mparser.ComparisonNode):
+ self.visit_default_func(node)
+ node.left.accept(self)
+ node.right.accept(self)
+
+ def visit_ArithmeticNode(self, node: mparser.ArithmeticNode):
+ self.visit_default_func(node)
+ node.left.accept(self)
+ node.right.accept(self)
+
+ def visit_NotNode(self, node: mparser.NotNode):
+ self.visit_default_func(node)
+ node.value.accept(self)
+
+ def visit_CodeBlockNode(self, node: mparser.CodeBlockNode):
+ self.visit_default_func(node)
+ for i in node.lines:
+ i.accept(self)
+
+ def visit_IndexNode(self, node: mparser.IndexNode):
+ self.visit_default_func(node)
+ node.index.accept(self)
+
+ def visit_MethodNode(self, node: mparser.MethodNode):
+ self.visit_default_func(node)
+ node.source_object.accept(self)
+ node.args.accept(self)
+
+ def visit_FunctionNode(self, node: mparser.FunctionNode):
+ self.visit_default_func(node)
+ node.args.accept(self)
+
+ def visit_AssignmentNode(self, node: mparser.AssignmentNode):
+ self.visit_default_func(node)
+ node.value.accept(self)
+
+ def visit_PlusAssignmentNode(self, node: mparser.PlusAssignmentNode):
+ self.visit_default_func(node)
+ node.value.accept(self)
+
+ def visit_ForeachClauseNode(self, node: mparser.ForeachClauseNode):
+ self.visit_default_func(node)
+ node.items.accept(self)
+ node.block.accept(self)
+
+ def visit_IfClauseNode(self, node: mparser.IfClauseNode):
+ self.visit_default_func(node)
+ for i in node.ifs:
+ i.accept(self)
+ if node.elseblock:
+ node.elseblock.accept(self)
+
+ def visit_UMinusNode(self, node: mparser.UMinusNode):
+ self.visit_default_func(node)
+ node.value.accept(self)
+
+ def visit_IfNode(self, node: mparser.IfNode):
+ self.visit_default_func(node)
+ node.condition.accept(self)
+ node.block.accept(self)
+
+ def visit_TernaryNode(self, node: mparser.TernaryNode):
+ self.visit_default_func(node)
+ node.condition.accept(self)
+ node.trueblock.accept(self)
+ node.falseblock.accept(self)
+
+ def visit_ArgumentNode(self, node: mparser.ArgumentNode):
+ self.visit_default_func(node)
+ for i in node.arguments:
+ i.accept(self)
+ for i in node.commas:
+ pass
+ for val in node.kwargs.values():
+ val.accept(self)
diff --git a/mesonbuild/mintro.py b/mesonbuild/mintro.py
index 36368af99..074c70a7d 100644
--- a/mesonbuild/mintro.py
+++ b/mesonbuild/mintro.py
@@ -21,14 +21,9 @@ project files and don't need this info."""
import json
from . import build, coredata as cdata
-from . import environment
from . import mesonlib
-from . import astinterpreter
-from . import mparser
+from .ast import IntrospectionInterpreter
from . import mlog
-from . import compilers
-from . import optinterpreter
-from .interpreterbase import InvalidArguments
from .backend import backends
import sys, os
import pathlib
@@ -151,122 +146,6 @@ def list_targets(builddata: build.Build, installdata, backend: backends.Backend)
tlist.append(t)
return tlist
-class IntrospectionHelper:
- # mimic an argparse namespace
- def __init__(self, cross_file):
- self.cross_file = cross_file
- self.native_file = None
- self.cmd_line_options = {}
-
-class IntrospectionInterpreter(astinterpreter.AstInterpreter):
- # Interpreter to detect the options without a build directory
- # Most of the code is stolen from interperter.Interpreter
- def __init__(self, source_root, subdir, backend, cross_file=None, subproject='', subproject_dir='subprojects', env=None):
- super().__init__(source_root, subdir)
-
- options = IntrospectionHelper(cross_file)
- self.cross_file = cross_file
- if env is None:
- self.environment = environment.Environment(source_root, None, options)
- else:
- self.environment = env
- self.subproject = subproject
- self.subproject_dir = subproject_dir
- self.coredata = self.environment.get_coredata()
- self.option_file = os.path.join(self.source_root, self.subdir, 'meson_options.txt')
- self.backend = backend
- self.default_options = {'backend': self.backend}
- self.project_data = {}
-
- self.funcs.update({
- 'project': self.func_project,
- 'add_languages': self.func_add_languages
- })
-
- def flatten_args(self, args):
- # Resolve mparser.ArrayNode if needed
- flattend_args = []
- if isinstance(args, mparser.ArrayNode):
- args = [x.value for x in args.args.arguments]
- for i in args:
- if isinstance(i, mparser.ArrayNode):
- flattend_args += [x.value for x in i.args.arguments]
- elif isinstance(i, str):
- flattend_args += [i]
- else:
- pass
- return flattend_args
-
- def func_project(self, node, args, kwargs):
- if len(args) < 1:
- raise InvalidArguments('Not enough arguments to project(). Needs at least the project name.')
-
- proj_name = args[0]
- proj_vers = kwargs.get('version', 'undefined')
- proj_langs = self.flatten_args(args[1:])
- if isinstance(proj_vers, mparser.ElementaryNode):
- proj_vers = proj_vers.value
- if not isinstance(proj_vers, str):
- proj_vers = 'undefined'
- self.project_data = {'descriptive_name': proj_name, 'version': proj_vers}
-
- if os.path.exists(self.option_file):
- oi = optinterpreter.OptionInterpreter(self.subproject)
- oi.process(self.option_file)
- self.coredata.merge_user_options(oi.options)
-
- def_opts = self.flatten_args(kwargs.get('default_options', []))
- self.project_default_options = mesonlib.stringlistify(def_opts)
- self.project_default_options = cdata.create_options_dict(self.project_default_options)
- self.default_options.update(self.project_default_options)
- self.coredata.set_default_options(self.default_options, self.subproject, self.environment.cmd_line_options)
-
- if not self.is_subproject() and 'subproject_dir' in kwargs:
- spdirname = kwargs['subproject_dir']
- if isinstance(spdirname, str):
- self.subproject_dir = spdirname
- if not self.is_subproject():
- self.project_data['subprojects'] = []
- subprojects_dir = os.path.join(self.source_root, self.subproject_dir)
- if os.path.isdir(subprojects_dir):
- for i in os.listdir(subprojects_dir):
- if os.path.isdir(os.path.join(subprojects_dir, i)):
- self.do_subproject(i)
-
- self.coredata.init_backend_options(self.backend)
- options = {k: v for k, v in self.environment.cmd_line_options.items() if k.startswith('backend_')}
-
- self.coredata.set_options(options)
- self.func_add_languages(None, proj_langs, None)
-
- def do_subproject(self, dirname):
- subproject_dir_abs = os.path.join(self.environment.get_source_dir(), self.subproject_dir)
- subpr = os.path.join(subproject_dir_abs, dirname)
- try:
- subi = IntrospectionInterpreter(subpr, '', self.backend, cross_file=self.cross_file, subproject=dirname, subproject_dir=self.subproject_dir, env=self.environment)
- subi.analyze()
- subi.project_data['name'] = dirname
- self.project_data['subprojects'] += [subi.project_data]
- except:
- return
-
- def func_add_languages(self, node, args, kwargs):
- args = self.flatten_args(args)
- need_cross_compiler = self.environment.is_cross_build()
- for lang in sorted(args, key=compilers.sort_clink):
- lang = lang.lower()
- if lang not in self.coredata.compilers:
- self.environment.detect_compilers(lang, need_cross_compiler)
-
- def is_subproject(self):
- return self.subproject != ''
-
- def analyze(self):
- self.load_root_meson_file()
- self.sanity_check_ast()
- self.parse_project()
- self.run()
-
def list_buildoptions_from_source(sourcedir, backend, indent):
# Make sure that log entries in other parts of meson don't interfere with the JSON output
mlog.disable()
diff --git a/mesonbuild/mparser.py b/mesonbuild/mparser.py
index fd8052e87..ec188372d 100644
--- a/mesonbuild/mparser.py
+++ b/mesonbuild/mparser.py
@@ -212,7 +212,15 @@ This will become a hard error in a future Meson release.""", self.getline(line_s
if not matched:
raise ParseException('lexer', self.getline(line_start), lineno, col)
-class ElementaryNode:
+class BaseNode:
+ def accept(self, visitor):
+ fname = 'visit_{}'.format(type(self).__name__)
+ if hasattr(visitor, fname):
+ func = getattr(visitor, fname)
+ if hasattr(func, '__call__'):
+ func(self)
+
+class ElementaryNode(BaseNode):
def __init__(self, token):
self.lineno = token.lineno
self.subdir = token.subdir
@@ -253,28 +261,28 @@ class ContinueNode(ElementaryNode):
class BreakNode(ElementaryNode):
pass
-class ArrayNode:
- def __init__(self, args):
+class ArrayNode(BaseNode):
+ def __init__(self, args, lineno, colno):
self.subdir = args.subdir
- self.lineno = args.lineno
- self.colno = args.colno
+ self.lineno = lineno
+ self.colno = colno
self.args = args
-class DictNode:
- def __init__(self, args):
+class DictNode(BaseNode):
+ def __init__(self, args, lineno, colno):
self.subdir = args.subdir
- self.lineno = args.lineno
- self.colno = args.colno
+ self.lineno = lineno
+ self.colno = colno
self.args = args
-class EmptyNode:
+class EmptyNode(BaseNode):
def __init__(self, lineno, colno):
self.subdir = ''
self.lineno = lineno
self.colno = colno
self.value = None
-class OrNode:
+class OrNode(BaseNode):
def __init__(self, left, right):
self.subdir = left.subdir
self.lineno = left.lineno
@@ -282,7 +290,7 @@ class OrNode:
self.left = left
self.right = right
-class AndNode:
+class AndNode(BaseNode):
def __init__(self, left, right):
self.subdir = left.subdir
self.lineno = left.lineno
@@ -290,7 +298,7 @@ class AndNode:
self.left = left
self.right = right
-class ComparisonNode:
+class ComparisonNode(BaseNode):
def __init__(self, ctype, left, right):
self.lineno = left.lineno
self.colno = left.colno
@@ -299,7 +307,7 @@ class ComparisonNode:
self.right = right
self.ctype = ctype
-class ArithmeticNode:
+class ArithmeticNode(BaseNode):
def __init__(self, operation, left, right):
self.subdir = left.subdir
self.lineno = left.lineno
@@ -308,21 +316,21 @@ class ArithmeticNode:
self.right = right
self.operation = operation
-class NotNode:
+class NotNode(BaseNode):
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:
+class CodeBlockNode(BaseNode):
def __init__(self, location_node):
self.subdir = location_node.subdir
self.lineno = location_node.lineno
self.colno = location_node.colno
self.lines = []
-class IndexNode:
+class IndexNode(BaseNode):
def __init__(self, iobject, index):
self.iobject = iobject
self.index = index
@@ -330,7 +338,7 @@ class IndexNode:
self.lineno = iobject.lineno
self.colno = iobject.colno
-class MethodNode:
+class MethodNode(BaseNode):
def __init__(self, subdir, lineno, colno, source_object, name, args):
self.subdir = subdir
self.lineno = lineno
@@ -340,7 +348,7 @@ class MethodNode:
assert(isinstance(self.name, str))
self.args = args
-class FunctionNode:
+class FunctionNode(BaseNode):
def __init__(self, subdir, lineno, colno, func_name, args):
self.subdir = subdir
self.lineno = lineno
@@ -349,7 +357,7 @@ class FunctionNode:
assert(isinstance(func_name, str))
self.args = args
-class AssignmentNode:
+class AssignmentNode(BaseNode):
def __init__(self, lineno, colno, var_name, value):
self.lineno = lineno
self.colno = colno
@@ -357,7 +365,7 @@ class AssignmentNode:
assert(isinstance(var_name, str))
self.value = value
-class PlusAssignmentNode:
+class PlusAssignmentNode(BaseNode):
def __init__(self, lineno, colno, var_name, value):
self.lineno = lineno
self.colno = colno
@@ -365,7 +373,7 @@ class PlusAssignmentNode:
assert(isinstance(var_name, str))
self.value = value
-class ForeachClauseNode:
+class ForeachClauseNode(BaseNode):
def __init__(self, lineno, colno, varnames, items, block):
self.lineno = lineno
self.colno = colno
@@ -373,28 +381,28 @@ class ForeachClauseNode:
self.items = items
self.block = block
-class IfClauseNode:
+class IfClauseNode(BaseNode):
def __init__(self, lineno, colno):
self.lineno = lineno
self.colno = colno
self.ifs = []
self.elseblock = EmptyNode(lineno, colno)
-class UMinusNode:
+class UMinusNode(BaseNode):
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:
+class IfNode(BaseNode):
def __init__(self, lineno, colno, condition, block):
self.lineno = lineno
self.colno = colno
self.condition = condition
self.block = block
-class TernaryNode:
+class TernaryNode(BaseNode):
def __init__(self, lineno, colno, condition, trueblock, falseblock):
self.lineno = lineno
self.colno = colno
@@ -402,7 +410,7 @@ class TernaryNode:
self.trueblock = trueblock
self.falseblock = falseblock
-class ArgumentNode:
+class ArgumentNode(BaseNode):
def __init__(self, token):
self.lineno = token.lineno
self.colno = token.colno
@@ -630,11 +638,11 @@ class Parser:
elif self.accept('lbracket'):
args = self.args()
self.block_expect('rbracket', block_start)
- return ArrayNode(args)
+ return ArrayNode(args, block_start.lineno, block_start.colno)
elif self.accept('lcurl'):
key_values = self.key_values()
self.block_expect('rcurl', block_start)
- return DictNode(key_values)
+ return DictNode(key_values, block_start.lineno, block_start.colno)
else:
return self.e9()
diff --git a/mesonbuild/rewriter.py b/mesonbuild/rewriter.py
index 37ed7efd6..277835c7c 100644
--- a/mesonbuild/rewriter.py
+++ b/mesonbuild/rewriter.py
@@ -23,36 +23,295 @@
# - move targets
# - reindent?
-import mesonbuild.astinterpreter
+from .ast import IntrospectionInterpreter, build_target_functions, AstIDGenerator, AstIndentationGenerator, AstPrinter
from mesonbuild.mesonlib import MesonException
-from mesonbuild import mlog
-import sys, traceback
+from . import mlog, mparser, environment
+from functools import wraps
+from pprint import pprint
+import json, os
+
+class RewriterException(MesonException):
+ pass
def add_arguments(parser):
parser.add_argument('--sourcedir', default='.',
help='Path to source directory.')
- parser.add_argument('--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='+')
+ parser.add_argument('-p', '--print', action='store_true', default=False, dest='print',
+ help='Print the parsed AST.')
+ parser.add_argument('command', type=str)
+
+class RequiredKeys:
+ def __init__(self, keys):
+ self.keys = keys
+
+ def __call__(self, f):
+ @wraps(f)
+ def wrapped(*wrapped_args, **wrapped_kwargs):
+ assert(len(wrapped_args) >= 2)
+ cmd = wrapped_args[1]
+ for key, val in self.keys.items():
+ typ = val[0] # The type of the value
+ default = val[1] # The default value -- None is required
+ choices = val[2] # Valid choices -- None is for everything
+ if key not in cmd:
+ if default is not None:
+ cmd[key] = default
+ else:
+ raise RewriterException('Key "{}" is missing in object for {}'
+ .format(key, f.__name__))
+ if not isinstance(cmd[key], typ):
+ raise RewriterException('Invalid type of "{}". Required is {} but provided was {}'
+ .format(key, typ.__name__, type(cmd[key]).__name__))
+ if choices is not None:
+ assert(isinstance(choices, list))
+ if cmd[key] not in choices:
+ raise RewriterException('Invalid value of "{}": Possible values are {} but provided was "{}"'
+ .format(key, choices, cmd[key]))
+ return f(*wrapped_args, **wrapped_kwargs)
+
+ return wrapped
+
+rewriter_keys = {
+ 'target': {
+ 'target': (str, None, None),
+ 'operation': (str, None, ['src_add', 'src_rm', 'test']),
+ 'sources': (list, [], None),
+ 'debug': (bool, False, None)
+ }
+}
+
+class Rewriter:
+ def __init__(self, sourcedir: str, generator: str = 'ninja'):
+ self.sourcedir = sourcedir
+ self.interpreter = IntrospectionInterpreter(sourcedir, '', generator)
+ self.id_generator = AstIDGenerator()
+ self.modefied_nodes = []
+ self.functions = {
+ 'target': self.process_target,
+ }
+
+ def analyze_meson(self):
+ mlog.log('Analyzing meson file:', mlog.bold(os.path.join(self.sourcedir, environment.build_filename)))
+ self.interpreter.analyze()
+ mlog.log(' -- Project:', mlog.bold(self.interpreter.project_data['descriptive_name']))
+ mlog.log(' -- Version:', mlog.cyan(self.interpreter.project_data['version']))
+ self.interpreter.ast.accept(AstIndentationGenerator())
+ self.interpreter.ast.accept(self.id_generator)
+
+ def find_target(self, target: str):
+ for i in self.interpreter.targets:
+ if target == i['name'] or target == i['id']:
+ return i
+ return None
+
+ @RequiredKeys(rewriter_keys['target'])
+ def process_target(self, cmd):
+ mlog.log('Processing target', mlog.bold(cmd['target']), 'operation', mlog.cyan(cmd['operation']))
+ target = self.find_target(cmd['target'])
+ if target is None:
+ mlog.error('Unknown target "{}" --> skipping'.format(cmd['target']))
+ if cmd['debug']:
+ pprint(self.interpreter.targets)
+ return
+ if cmd['debug']:
+ pprint(target)
+
+ # Utility function to get a list of the sources from a node
+ def arg_list_from_node(n):
+ args = []
+ if isinstance(n, mparser.FunctionNode):
+ args = list(n.args.arguments)
+ if n.func_name in build_target_functions:
+ args.pop(0)
+ elif isinstance(n, mparser.ArrayNode):
+ args = n.args.arguments
+ elif isinstance(n, mparser.ArgumentNode):
+ args = n.arguments
+ return args
+
+ if cmd['operation'] == 'src_add':
+ node = None
+ if target['sources']:
+ node = target['sources'][0]
+ else:
+ node = target['node']
+ assert(node is not None)
+
+ # Generate the new String nodes
+ to_append = []
+ for i in cmd['sources']:
+ mlog.log(' -- Adding source', mlog.green(i), 'at',
+ mlog.yellow('{}:{}'.format(os.path.join(node.subdir, environment.build_filename), node.lineno)))
+ token = mparser.Token('string', node.subdir, 0, 0, 0, None, i)
+ to_append += [mparser.StringNode(token)]
+
+ # Append to the AST at the right place
+ if isinstance(node, mparser.FunctionNode):
+ node.args.arguments += to_append
+ elif isinstance(node, mparser.ArrayNode):
+ node.args.arguments += to_append
+ elif isinstance(node, mparser.ArgumentNode):
+ node.arguments += to_append
+
+ # Mark the node as modified
+ if node not in self.modefied_nodes:
+ self.modefied_nodes += [node]
+
+ elif cmd['operation'] == 'src_rm':
+ # Helper to find the exact string node and its parent
+ def find_node(src):
+ for i in target['sources']:
+ for j in arg_list_from_node(i):
+ if isinstance(j, mparser.StringNode):
+ if j.value == src:
+ return i, j
+ return None, None
+
+ for i in cmd['sources']:
+ # Try to find the node with the source string
+ root, string_node = find_node(i)
+ if root is None:
+ mlog.warning(' -- Unable to find source', mlog.green(i), 'in the target')
+ continue
+
+ # Remove the found string node from the argument list
+ arg_node = None
+ if isinstance(root, mparser.FunctionNode):
+ arg_node = root.args
+ if isinstance(root, mparser.ArrayNode):
+ arg_node = root.args
+ if isinstance(root, mparser.ArgumentNode):
+ arg_node = root
+ assert(arg_node is not None)
+ mlog.log(' -- Removing source', mlog.green(i), 'from',
+ mlog.yellow('{}:{}'.format(os.path.join(string_node.subdir, environment.build_filename), string_node.lineno)))
+ arg_node.arguments.remove(string_node)
+
+ # Mark the node as modified
+ if root not in self.modefied_nodes:
+ self.modefied_nodes += [root]
+
+ elif cmd['operation'] == 'test':
+ # List all sources in the target
+ src_list = []
+ for i in target['sources']:
+ for j in arg_list_from_node(i):
+ if isinstance(j, mparser.StringNode):
+ src_list += [j.value]
+ test_data = {
+ 'name': target['name'],
+ 'sources': src_list
+ }
+ mlog.log(' !! target {}={}'.format(target['id'], json.dumps(test_data)))
+
+ def process(self, cmd):
+ if 'type' not in cmd:
+ raise RewriterException('Command has no key "type"')
+ if cmd['type'] not in self.functions:
+ raise RewriterException('Unknown command "{}". Supported commands are: {}'
+ .format(cmd['type'], list(self.functions.keys())))
+ self.functions[cmd['type']](cmd)
+
+ def apply_changes(self):
+ assert(all(hasattr(x, 'lineno') and hasattr(x, 'colno') and hasattr(x, 'subdir') for x in self.modefied_nodes))
+ assert(all(isinstance(x, (mparser.ArrayNode, mparser.FunctionNode)) for x in self.modefied_nodes))
+ # Sort based on line and column in reversed order
+ work_nodes = list(sorted(self.modefied_nodes, key=lambda x: x.lineno * 1000 + x.colno, reverse=True))
+
+ # Generating the new replacement string
+ str_list = []
+ for i in work_nodes:
+ printer = AstPrinter()
+ i.accept(printer)
+ printer.post_process()
+ data = {
+ 'file': os.path.join(i.subdir, environment.build_filename),
+ 'str': printer.result.strip(),
+ 'node': i
+ }
+ str_list += [data]
+
+ # Load build files
+ files = {}
+ for i in str_list:
+ if i['file'] in files:
+ continue
+ fpath = os.path.realpath(os.path.join(self.sourcedir, i['file']))
+ fdata = ''
+ with open(fpath, 'r') as fp:
+ fdata = fp.read()
+
+ # Generate line offsets numbers
+ m_lines = fdata.splitlines(True)
+ offset = 0
+ line_offsets = []
+ for j in m_lines:
+ line_offsets += [offset]
+ offset += len(j)
+
+ files[i['file']] = {
+ 'path': fpath,
+ 'raw': fdata,
+ 'offsets': line_offsets
+ }
+
+ # Replace in source code
+ for i in str_list:
+ offsets = files[i['file']]['offsets']
+ raw = files[i['file']]['raw']
+ node = i['node']
+ line = node.lineno - 1
+ col = node.colno
+ start = offsets[line] + col
+ end = start
+ if isinstance(node, mparser.ArrayNode):
+ if raw[end] != '[':
+ mlog.warning('Internal error: expected "[" at {}:{} but got "{}"'.format(line, col, raw[end]))
+ continue
+ counter = 1
+ while counter > 0:
+ end += 1
+ if raw[end] == '[':
+ counter += 1
+ elif raw[end] == ']':
+ counter -= 1
+ end += 1
+ elif isinstance(node, mparser.FunctionNode):
+ while raw[end] != '(':
+ end += 1
+ end += 1
+ counter = 1
+ while counter > 0:
+ end += 1
+ if raw[end] == '(':
+ counter += 1
+ elif raw[end] == ')':
+ counter -= 1
+ end += 1
+ raw = files[i['file']]['raw'] = raw[:start] + i['str'] + raw[end:]
+
+ # Write the files back
+ for key, val in files.items():
+ mlog.log('Rewriting', mlog.yellow(key))
+ with open(val['path'], 'w') as fp:
+ fp.write(val['raw'])
def run(options):
- 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.RewriterInterpreter(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):
- mlog.exception(e)
- else:
- traceback.print_exc()
- return 1
+ rewriter = Rewriter(options.sourcedir)
+ rewriter.analyze_meson()
+ if os.path.exists(options.command):
+ with open(options.command, 'r') as fp:
+ commands = json.load(fp)
+ else:
+ commands = json.loads(options.command)
+
+ if not isinstance(commands, list):
+ raise TypeError('Command is not a list')
+
+ for i in commands:
+ if not isinstance(i, object):
+ raise TypeError('Command is not an object')
+ rewriter.process(i)
+
+ rewriter.apply_changes()
return 0
diff --git a/run_unittests.py b/run_unittests.py
index d97ae7eed..abedf4ae8 100755
--- a/run_unittests.py
+++ b/run_unittests.py
@@ -33,6 +33,7 @@ from configparser import ConfigParser
from contextlib import contextmanager
from glob import glob
from pathlib import (PurePath, Path)
+from distutils.dir_util import copy_tree
import mesonbuild.mlog
import mesonbuild.compilers
@@ -1014,6 +1015,7 @@ class BasePlatformTests(unittest.TestCase):
self.mconf_command = self.meson_command + ['configure']
self.mintro_command = self.meson_command + ['introspect']
self.wrap_command = self.meson_command + ['wrap']
+ self.rewrite_command = self.meson_command + ['rewrite']
# Backend-specific build commands
self.build_command, self.clean_command, self.test_command, self.install_command, \
self.uninstall_command = get_backend_commands(self.backend)
@@ -1022,6 +1024,7 @@ class BasePlatformTests(unittest.TestCase):
self.vala_test_dir = os.path.join(src_root, 'test cases/vala')
self.framework_test_dir = os.path.join(src_root, 'test cases/frameworks')
self.unit_test_dir = os.path.join(src_root, 'test cases/unit')
+ self.rewrite_test_dir = os.path.join(src_root, 'test cases/rewrite')
# Misc stuff
self.orig_env = os.environ.copy()
if self.backend is Backend.ninja:
@@ -4967,68 +4970,115 @@ class PythonTests(BasePlatformTests):
self.wipe()
-class RewriterTests(unittest.TestCase):
+class RewriterTests(BasePlatformTests):
+ data_regex = re.compile(r'^\s*!!\s*(\w+)\s+([^=]+)=(.*)$')
def setUp(self):
super().setUp()
- src_root = os.path.dirname(__file__)
- self.testroot = os.path.realpath(tempfile.mkdtemp())
- self.rewrite_command = python_command + [os.path.join(src_root, 'mesonrewriter.py')]
- self.tmpdir = os.path.realpath(tempfile.mkdtemp())
- self.workdir = os.path.join(self.tmpdir, 'foo')
- self.test_dir = os.path.join(src_root, 'test cases/rewrite')
+ self.maxDiff = None
- def tearDown(self):
- windows_proof_rmtree(self.tmpdir)
+ def prime(self, dirname):
+ copy_tree(os.path.join(self.rewrite_test_dir, dirname), self.builddir)
- def read_contents(self, fname):
- with open(os.path.join(self.workdir, fname)) as f:
- return f.read()
+ def rewrite(self, directory, args):
+ if isinstance(args, str):
+ args = [args]
+ out = subprocess.check_output(self.rewrite_command + ['--sourcedir', directory] + args,
+ universal_newlines=True)
+ return out
- 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 extract_test_data(self, out):
+ lines = out.split('\n')
+ result = {}
+ for i in lines:
+ match = RewriterTests.data_regex.match(i)
+ if match:
+ typ = match.group(1)
+ id = match.group(2)
+ data = json.loads(match.group(3))
+ if typ not in result:
+ result[typ] = {}
+ result[typ][id] = data
+ return result
- def prime(self, dirname):
- shutil.copytree(os.path.join(self.test_dir, dirname), self.workdir)
+ def test_target_source_list(self):
+ self.prime('1 basic')
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
+ out = self.extract_test_data(out)
+ expected = {
+ 'target': {
+ 'trivialprog1@exe': {'name': 'trivialprog1', 'sources': ['main.cpp', 'fileA.cpp']},
+ 'trivialprog2@exe': {'name': 'trivialprog2', 'sources': ['fileB.cpp', 'fileC.cpp']},
+ 'trivialprog3@exe': {'name': 'trivialprog3', 'sources': ['main.cpp', 'fileA.cpp']},
+ 'trivialprog4@exe': {'name': 'trivialprog4', 'sources': ['main.cpp', 'fileA.cpp']},
+ 'trivialprog5@exe': {'name': 'trivialprog5', 'sources': ['main.cpp', 'fileB.cpp', 'fileC.cpp']},
+ 'trivialprog6@exe': {'name': 'trivialprog6', 'sources': ['main.cpp', 'fileA.cpp']},
+ 'trivialprog7@exe': {'name': 'trivialprog7', 'sources': ['fileB.cpp', 'fileC.cpp', 'main.cpp', 'fileA.cpp']},
+ 'trivialprog8@exe': {'name': 'trivialprog8', 'sources': ['main.cpp', 'fileA.cpp']},
+ 'trivialprog9@exe': {'name': 'trivialprog9', 'sources': ['main.cpp', 'fileA.cpp']},
+ }
+ }
+ self.assertDictEqual(out, expected)
- def test_basic(self):
+ def test_target_add_sources(self):
self.prime('1 basic')
- subprocess.check_call(self.rewrite_command + ['remove',
- '--target=trivialprog',
- '--filename=notthere.c',
- '--sourcedir', self.workdir],
- universal_newlines=True)
- self.check_effectively_same('meson.build', 'removed.txt')
- subprocess.check_call(self.rewrite_command + ['add',
- '--target=trivialprog',
- '--filename=notthere.c',
- '--sourcedir', self.workdir],
- universal_newlines=True)
- self.check_effectively_same('meson.build', 'added.txt')
- subprocess.check_call(self.rewrite_command + ['remove',
- '--target=trivialprog',
- '--filename=notthere.c',
- '--sourcedir', self.workdir],
- universal_newlines=True)
- self.check_effectively_same('meson.build', 'removed.txt')
-
- def test_subdir(self):
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'addSrc.json'))
+ out = self.extract_test_data(out)
+ expected = {
+ 'target': {
+ 'trivialprog1@exe': {'name': 'trivialprog1', 'sources': ['main.cpp', 'fileA.cpp', 'a1.cpp', 'a2.cpp', 'a6.cpp']},
+ 'trivialprog2@exe': {'name': 'trivialprog2', 'sources': ['fileB.cpp', 'fileC.cpp', 'a7.cpp']},
+ 'trivialprog3@exe': {'name': 'trivialprog3', 'sources': ['main.cpp', 'fileA.cpp', 'a5.cpp']},
+ 'trivialprog4@exe': {'name': 'trivialprog4', 'sources': ['main.cpp', 'a5.cpp', 'fileA.cpp']},
+ 'trivialprog5@exe': {'name': 'trivialprog5', 'sources': ['main.cpp', 'a3.cpp', 'fileB.cpp', 'fileC.cpp', 'a7.cpp']},
+ 'trivialprog6@exe': {'name': 'trivialprog6', 'sources': ['main.cpp', 'fileA.cpp', 'a4.cpp']},
+ 'trivialprog7@exe': {'name': 'trivialprog7', 'sources': ['fileB.cpp', 'fileC.cpp', 'main.cpp', 'fileA.cpp', 'a1.cpp', 'a2.cpp', 'a6.cpp']},
+ 'trivialprog8@exe': {'name': 'trivialprog8', 'sources': ['main.cpp', 'fileA.cpp', 'a1.cpp', 'a2.cpp', 'a6.cpp']},
+ 'trivialprog9@exe': {'name': 'trivialprog9', 'sources': ['main.cpp', 'fileA.cpp', 'a1.cpp', 'a2.cpp', 'a6.cpp']},
+ }
+ }
+ self.assertDictEqual(out, expected)
+
+ # Check the written file
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
+ out = self.extract_test_data(out)
+ self.assertDictEqual(out, expected)
+
+ def test_target_remove_sources(self):
+ self.prime('1 basic')
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'rmSrc.json'))
+ out = self.extract_test_data(out)
+ expected = {
+ 'target': {
+ 'trivialprog1@exe': {'name': 'trivialprog1', 'sources': ['main.cpp']},
+ 'trivialprog2@exe': {'name': 'trivialprog2', 'sources': ['fileC.cpp']},
+ 'trivialprog3@exe': {'name': 'trivialprog3', 'sources': ['main.cpp']},
+ 'trivialprog4@exe': {'name': 'trivialprog4', 'sources': ['main.cpp']},
+ 'trivialprog5@exe': {'name': 'trivialprog5', 'sources': ['main.cpp', 'fileC.cpp']},
+ 'trivialprog6@exe': {'name': 'trivialprog6', 'sources': ['main.cpp']},
+ 'trivialprog7@exe': {'name': 'trivialprog7', 'sources': ['fileC.cpp', 'main.cpp']},
+ 'trivialprog8@exe': {'name': 'trivialprog8', 'sources': ['main.cpp']},
+ 'trivialprog9@exe': {'name': 'trivialprog9', 'sources': ['main.cpp']},
+ }
+ }
+ self.assertDictEqual(out, expected)
+
+ # Check the written file
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
+ out = self.extract_test_data(out)
+ self.assertDictEqual(out, expected)
+
+ def test_target_subdir(self):
self.prime('2 subdirs')
- top = self.read_contents('meson.build')
- s2 = self.read_contents('sub2/meson.build')
- subprocess.check_call(self.rewrite_command + ['remove',
- '--target=something',
- '--filename=second.c',
- '--sourcedir', self.workdir],
- universal_newlines=True)
- 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'))
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'addSrc.json'))
+ out = self.extract_test_data(out)
+ expected = {'name': 'something', 'sources': ['first.c', 'second.c', 'third.c']}
+ self.assertDictEqual(list(out['target'].values())[0], expected)
+ # Check the written file
+ out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
+ out = self.extract_test_data(out)
+ self.assertDictEqual(list(out['target'].values())[0], expected)
class NativeFileTests(BasePlatformTests):
@@ -5321,7 +5371,7 @@ def should_run_cross_mingw_tests():
def main():
unset_envs()
cases = ['InternalTests', 'DataTests', 'AllPlatformTests', 'FailureTests',
- 'PythonTests', 'NativeFileTests']
+ 'PythonTests', 'NativeFileTests', 'RewriterTests']
if not is_windows():
cases += ['LinuxlikeTests']
if should_run_cross_arm_tests():
diff --git a/setup.py b/setup.py
index f1f2e81c1..f35296030 100644
--- a/setup.py
+++ b/setup.py
@@ -28,6 +28,7 @@ from setuptools import setup
# Other platforms will create bin/meson
entries = {'console_scripts': ['meson=mesonbuild.mesonmain:main']}
packages = ['mesonbuild',
+ 'mesonbuild.ast',
'mesonbuild.backend',
'mesonbuild.compilers',
'mesonbuild.dependencies',
diff --git a/test cases/rewrite/1 basic/addSrc.json b/test cases/rewrite/1 basic/addSrc.json
new file mode 100644
index 000000000..1a504bff2
--- /dev/null
+++ b/test cases/rewrite/1 basic/addSrc.json
@@ -0,0 +1,89 @@
+[
+ {
+ "type": "target",
+ "target": "trivialprog1",
+ "operation": "src_add",
+ "sources": ["a1.cpp", "a2.cpp"]
+ },
+ {
+ "type": "target",
+ "target": "trivialprog2",
+ "operation": "src_add",
+ "sources": ["a7.cpp"]
+ },
+ {
+ "type": "target",
+ "target": "trivialprog3",
+ "operation": "src_add",
+ "sources": ["a5.cpp"]
+ },
+ {
+ "type": "target",
+ "target": "trivialprog4",
+ "operation": "src_add",
+ "sources": ["a5.cpp"]
+ },
+ {
+ "type": "target",
+ "target": "trivialprog5",
+ "operation": "src_add",
+ "sources": ["a3.cpp"]
+ },
+ {
+ "type": "target",
+ "target": "trivialprog6",
+ "operation": "src_add",
+ "sources": ["a4.cpp"]
+ },
+ {
+ "type": "target",
+ "target": "trivialprog9",
+ "operation": "src_add",
+ "sources": ["a6.cpp"]
+ },
+ {
+ "type": "target",
+ "target": "trivialprog1",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog2",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog3",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog4",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog5",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog6",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog7",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog8",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog9",
+ "operation": "test"
+ }
+]
diff --git a/test cases/rewrite/1 basic/added.txt b/test cases/rewrite/1 basic/added.txt
deleted file mode 100644
index 657dd42fb..000000000
--- a/test cases/rewrite/1 basic/added.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-project('rewritetest', 'c')
-
-sources = ['trivial.c']
-
-exe = executable('trivialprog', 'notthere.c', sources)
diff --git a/test cases/rewrite/1 basic/info.json b/test cases/rewrite/1 basic/info.json
new file mode 100644
index 000000000..be2a87384
--- /dev/null
+++ b/test cases/rewrite/1 basic/info.json
@@ -0,0 +1,47 @@
+[
+ {
+ "type": "target",
+ "target": "trivialprog1",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog2",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog3",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog4",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog5",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog6",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog7",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog8",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog9",
+ "operation": "test"
+ }
+]
diff --git a/test cases/rewrite/1 basic/meson.build b/test cases/rewrite/1 basic/meson.build
index a0485d079..1bed0e18d 100644
--- a/test cases/rewrite/1 basic/meson.build
+++ b/test cases/rewrite/1 basic/meson.build
@@ -1,5 +1,18 @@
-project('rewritetest', 'c')
+project('rewritetest', 'cpp')
-sources = ['trivial.c', 'notthere.c']
+src1 = ['main.cpp', 'fileA.cpp']
+src2 = files(['fileB.cpp', 'fileC.cpp'])
+src3 = src1
+src4 = [src3]
-exe = executable('trivialprog', sources)
+# Magic comment
+
+exe1 = executable('trivialprog1', src1)
+exe2 = executable('trivialprog2', [src2])
+exe3 = executable('trivialprog3', ['main.cpp', 'fileA.cpp'])
+exe4 = executable('trivialprog4', ['main.cpp', ['fileA.cpp']])
+exe5 = executable('trivialprog5', [src2, 'main.cpp'])
+exe6 = executable('trivialprog6', 'main.cpp', 'fileA.cpp')
+exe7 = executable('trivialprog7', 'fileB.cpp', src1, 'fileC.cpp')
+exe8 = executable('trivialprog8', src3)
+exe9 = executable('trivialprog9', src4)
diff --git a/test cases/rewrite/1 basic/removed.txt b/test cases/rewrite/1 basic/removed.txt
deleted file mode 100644
index 55192149c..000000000
--- a/test cases/rewrite/1 basic/removed.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-project('rewritetest', 'c')
-
-sources = ['trivial.c']
-
-exe = executable('trivialprog', sources)
diff --git a/test cases/rewrite/1 basic/rmSrc.json b/test cases/rewrite/1 basic/rmSrc.json
new file mode 100644
index 000000000..a8559a570
--- /dev/null
+++ b/test cases/rewrite/1 basic/rmSrc.json
@@ -0,0 +1,83 @@
+[
+ {
+ "type": "target",
+ "target": "trivialprog1",
+ "operation": "src_rm",
+ "sources": ["fileA.cpp"]
+ },
+ {
+ "type": "target",
+ "target": "trivialprog3",
+ "operation": "src_rm",
+ "sources": ["fileA.cpp"]
+ },
+ {
+ "type": "target",
+ "target": "trivialprog4",
+ "operation": "src_rm",
+ "sources": ["fileA.cpp"]
+ },
+ {
+ "type": "target",
+ "target": "trivialprog5",
+ "operation": "src_rm",
+ "sources": ["fileB.cpp"]
+ },
+ {
+ "type": "target",
+ "target": "trivialprog6",
+ "operation": "src_rm",
+ "sources": ["fileA.cpp"]
+ },
+ {
+ "type": "target",
+ "target": "trivialprog7",
+ "operation": "src_rm",
+ "sources": ["fileB.cpp"]
+ },
+ {
+ "type": "target",
+ "target": "trivialprog1",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog2",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog3",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog4",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog5",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog6",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog7",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog8",
+ "operation": "test"
+ },
+ {
+ "type": "target",
+ "target": "trivialprog9",
+ "operation": "test"
+ }
+]
diff --git a/test cases/rewrite/2 subdirs/addSrc.json b/test cases/rewrite/2 subdirs/addSrc.json
new file mode 100644
index 000000000..017476c60
--- /dev/null
+++ b/test cases/rewrite/2 subdirs/addSrc.json
@@ -0,0 +1,13 @@
+[
+ {
+ "type": "target",
+ "target": "something",
+ "operation": "src_add",
+ "sources": ["third.c"]
+ },
+ {
+ "type": "target",
+ "target": "something",
+ "operation": "test"
+ }
+]
diff --git a/test cases/rewrite/2 subdirs/info.json b/test cases/rewrite/2 subdirs/info.json
new file mode 100644
index 000000000..01733333f
--- /dev/null
+++ b/test cases/rewrite/2 subdirs/info.json
@@ -0,0 +1,7 @@
+[
+ {
+ "type": "target",
+ "target": "something",
+ "operation": "test"
+ }
+]
diff --git a/test cases/rewrite/2 subdirs/meson.build b/test cases/rewrite/2 subdirs/meson.build
index 79b7ad738..c7f3fec89 100644
--- a/test cases/rewrite/2 subdirs/meson.build
+++ b/test cases/rewrite/2 subdirs/meson.build
@@ -2,4 +2,3 @@ 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
deleted file mode 100644
index 53ceaffd3..000000000
--- a/test cases/rewrite/2 subdirs/sub1/after.txt
+++ /dev/null
@@ -1 +0,0 @@
-srcs = ['first.c']
diff --git a/test cases/rewrite/2 subdirs/sub2/meson.build b/test cases/rewrite/2 subdirs/sub2/meson.build
index 0d92e7f42..44b4075ea 100644
--- a/test cases/rewrite/2 subdirs/sub2/meson.build
+++ b/test cases/rewrite/2 subdirs/sub2/meson.build
@@ -1,2 +1 @@
executable('something', srcs)
-