diff options
author | Jussi Pakkanen <jpakkane@gmail.com> | 2019-01-27 20:46:29 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-01-27 20:46:29 +0200 |
commit | 733f9a77652ecb322fff28e46ef11762241bdb09 (patch) | |
tree | 5f515fe6a44eb19d14401d90672e627063b78456 | |
parent | 2dcb6eb0b3d7574b9fa97f962b715bebe1d043ef (diff) | |
parent | dbb94f122ddeb5a37ff2603acbcc701b996958bb (diff) | |
download | meson-733f9a77652ecb322fff28e46ef11762241bdb09.tar.gz |
Merge pull request #4814 from mensinda/astVisitor
rewriter: Rewrote the meson rewriter - now works with AST modification
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(): @@ -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) - |