diff options
author | Jussi Pakkanen <jpakkane@gmail.com> | 2018-11-27 22:18:13 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-11-27 22:18:13 +0200 |
commit | 270f2395d6f4d3f1134393bf18c8eb3d9f1e5332 (patch) | |
tree | 8717aa86b33607d6c71be4cfa1d2e553538cfbf3 | |
parent | 2c91ca3d6a39692071a2b9f9c23b7c378a3e837e (diff) | |
parent | f954eee90669f4c9ac0f458975a37ace28ba3a47 (diff) | |
download | meson-270f2395d6f4d3f1134393bf18c8eb3d9f1e5332.tar.gz |
Merge pull request #4191 from trilader/feature/projectinfo-from-source
mintro: Allow introspect --projectinfo without build directory
-rw-r--r-- | docs/markdown/snippets/introspect_projectinfo.md | 35 | ||||
-rw-r--r-- | mesonbuild/astinterpreter.py | 94 | ||||
-rw-r--r-- | mesonbuild/mintro.py | 77 | ||||
-rw-r--r-- | mesonbuild/rewriter.py | 2 | ||||
-rwxr-xr-x | run_unittests.py | 34 |
5 files changed, 210 insertions, 32 deletions
diff --git a/docs/markdown/snippets/introspect_projectinfo.md b/docs/markdown/snippets/introspect_projectinfo.md new file mode 100644 index 000000000..40558b88e --- /dev/null +++ b/docs/markdown/snippets/introspect_projectinfo.md @@ -0,0 +1,35 @@ +## `introspect --projectinfo` can now be used without configured build directory + +This allows IDE integration to get information about the project before the user has configured a build directory. + +Before you could use `meson.py introspect --projectinfo build-directory`. +Now you also can use `meson.py introspect --projectinfo project-dir/meson.build`. + +The output is similiar to the output with a build directory but additionally also includes information from `introspect --buildsystem-files`. + +For example `meson.py introspect --projectinfo test\ cases/common/47\ subproject\ options/meson.build` +This outputs (pretty printed for readability): +``` +{ + "buildsystem_files": [ + "meson_options.txt", + "meson.build" + ], + "name": "suboptions", + "version": null, + "descriptive_name": "suboptions", + "subprojects": [ + { + "buildsystem_files": [ + "subprojects/subproject/meson_options.txt", + "subprojects/subproject/meson.build" + ], + "name": "subproject", + "version": "undefined", + "descriptive_name": "subproject" + } + ] +} +``` + +Both usages now include a new `descriptive_name` property which always shows the name set in the project. diff --git a/mesonbuild/astinterpreter.py b/mesonbuild/astinterpreter.py index 32d08450c..a447a5597 100644 --- a/mesonbuild/astinterpreter.py +++ b/mesonbuild/astinterpreter.py @@ -18,7 +18,7 @@ from . import interpreterbase, mlog, mparser, mesonlib from . import environment -from .interpreterbase import InterpreterException, InvalidArguments +from .interpreterbase import InterpreterException, InvalidArguments, BreakRequest, ContinueRequest import os, sys @@ -46,7 +46,6 @@ REMOVE_SOURCE = 1 class AstInterpreter(interpreterbase.InterpreterBase): def __init__(self, source_root, subdir): super().__init__(source_root, subdir) - self.asts = {} self.funcs.update({'project': self.func_do_nothing, 'test': self.func_do_nothing, 'benchmark': self.func_do_nothing, @@ -76,7 +75,72 @@ class AstInterpreter(interpreterbase.InterpreterBase): 'vcs_tag': self.func_do_nothing, 'add_languages': self.func_do_nothing, 'declare_dependency': self.func_do_nothing, - 'files': self.func_files, + 'files': self.func_do_nothing, + 'executable': self.func_do_nothing, + 'static_library': self.func_do_nothing, + 'shared_library': self.func_do_nothing, + 'library': self.func_do_nothing, + 'build_target': self.func_do_nothing, + 'custom_target': self.func_do_nothing, + 'run_target': self.func_do_nothing, + 'subdir': self.func_do_nothing, + 'set_variable': self.func_do_nothing, + 'get_variable': self.func_do_nothing, + 'is_variable': self.func_do_nothing, + }) + + def func_do_nothing(self, node, args, kwargs): + return True + + def method_call(self, node): + return True + + def evaluate_arithmeticstatement(self, cur): + return 0 + + def evaluate_plusassign(self, node): + return 0 + + def evaluate_indexing(self, node): + return 0 + + def unknown_function_called(self, func_name): + pass + + def reduce_arguments(self, args): + assert(isinstance(args, mparser.ArgumentNode)) + if args.incorrect_order(): + raise InvalidArguments('All keyword arguments must be after positional arguments.') + return args.arguments, args.kwargs + + def evaluate_comparison(self, node): + return False + + def evaluate_foreach(self, node): + try: + self.evaluate_codeblock(node.block) + except ContinueRequest: + pass + except BreakRequest: + pass + + def evaluate_if(self, node): + for i in node.ifs: + self.evaluate_codeblock(i.block) + if not isinstance(node.elseblock, mparser.EmptyNode): + self.evaluate_codeblock(node.elseblock) + + def get_variable(self, varname): + 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, @@ -90,12 +154,6 @@ class AstInterpreter(interpreterbase.InterpreterBase): 'is_variable': self.func_is_variable, }) - def func_do_nothing(self, node, args, kwargs): - return True - - def method_call(self, node): - return True - def func_executable(self, node, args, kwargs): if args[0] == self.targetname: if self.operation == ADD_SOURCE: @@ -147,21 +205,6 @@ class AstInterpreter(interpreterbase.InterpreterBase): return [args] return args - def evaluate_arithmeticstatement(self, cur): - return 0 - - def evaluate_plusassign(self, node): - return 0 - - def evaluate_indexing(self, node): - return 0 - - def reduce_arguments(self, args): - assert(isinstance(args, mparser.ArgumentNode)) - if args.incorrect_order(): - raise InvalidArguments('All keyword arguments must be after positional arguments.') - return args.arguments, args.kwargs - def transform(self): self.load_root_meson_file() self.asts[''] = self.ast @@ -181,9 +224,6 @@ class AstInterpreter(interpreterbase.InterpreterBase): self.filename = filename self.transform() - def unknown_function_called(self, func_name): - mlog.warning('Unknown function called: ' + func_name) - def add_source_to_target(self, node, args, kwargs): namespan = node.args.arguments[0].bytespan buildfilename = os.path.join(self.source_root, self.subdir, environment.build_filename) diff --git a/mesonbuild/mintro.py b/mesonbuild/mintro.py index 4062299c0..48ec20faf 100644 --- a/mesonbuild/mintro.py +++ b/mesonbuild/mintro.py @@ -22,6 +22,9 @@ project files and don't need this info.""" import json from . import build, mtest, coredata as cdata from . import mesonlib +from . import astinterpreter +from . import mparser +from .interpreterbase import InvalidArguments from .backend import ninjabackend import sys, os import pathlib @@ -178,14 +181,18 @@ def add_keys(optlist, options, section): optdict['description'] = opt.description optlist.append(optdict) -def list_buildsystem_files(builddata): - src_dir = builddata.environment.get_source_dir() +def find_buildsystem_files_list(src_dir): # I feel dirty about this. But only slightly. filelist = [] for root, _, files in os.walk(src_dir): for f in files: if f == 'meson.build' or f == 'meson_options.txt': filelist.append(os.path.relpath(os.path.join(root, f), src_dir)) + return filelist + +def list_buildsystem_files(builddata): + src_dir = builddata.environment.get_source_dir() + filelist = find_buildsystem_files_list(src_dir) print(json.dumps(filelist)) def list_deps(coredata): @@ -219,19 +226,81 @@ def list_tests(testdata): print(json.dumps(result)) def list_projinfo(builddata): - result = {'name': builddata.project_name, 'version': builddata.project_version} + result = {'version': builddata.project_version, + 'descriptive_name': builddata.project_name} subprojects = [] for k, v in builddata.subprojects.items(): c = {'name': k, - 'version': v} + 'version': v, + 'descriptive_name': builddata.projects.get(k)} subprojects.append(c) result['subprojects'] = subprojects print(json.dumps(result)) +class ProjectInfoInterperter(astinterpreter.AstInterpreter): + def __init__(self, source_root, subdir): + super().__init__(source_root, subdir) + self.funcs.update({'project': self.func_project}) + self.project_name = None + self.project_version = None + + def func_project(self, node, args, kwargs): + if len(args) < 1: + raise InvalidArguments('Not enough arguments to project(). Needs at least the project name.') + self.project_name = args[0] + self.project_version = kwargs.get('version', 'undefined') + if isinstance(self.project_version, mparser.ElementaryNode): + self.project_version = self.project_version.value + + def set_variable(self, varname, variable): + pass + + def analyze(self): + self.load_root_meson_file() + self.sanity_check_ast() + self.parse_project() + self.run() + +def list_projinfo_from_source(sourcedir): + files = find_buildsystem_files_list(sourcedir) + + result = {'buildsystem_files': []} + subprojects = {} + + for f in files: + f = f.replace('\\', '/') + if f == 'meson.build': + interpreter = ProjectInfoInterperter(sourcedir, '') + interpreter.analyze() + version = None + if interpreter.project_version is str: + version = interpreter.project_version + result.update({'version': version, 'descriptive_name': interpreter.project_name}) + result['buildsystem_files'].append(f) + elif f.startswith('subprojects/'): + subproject_id = f.split('/')[1] + subproject = subprojects.setdefault(subproject_id, {'buildsystem_files': []}) + subproject['buildsystem_files'].append(f) + if f.count('/') == 2 and f.endswith('meson.build'): + interpreter = ProjectInfoInterperter(os.path.join(sourcedir, 'subprojects', subproject_id), '') + interpreter.analyze() + subproject.update({'name': subproject_id, 'version': interpreter.project_version, 'descriptive_name': interpreter.project_name}) + else: + result['buildsystem_files'].append(f) + + subprojects = [obj for name, obj in subprojects.items()] + result['subprojects'] = subprojects + print(json.dumps(result)) + def run(options): datadir = 'meson-private' if options.builddir is not None: datadir = os.path.join(options.builddir, datadir) + if options.builddir.endswith('/meson.build') or options.builddir.endswith('\\meson.build') or options.builddir == 'meson.build': + if options.projectinfo: + sourcedir = '.' if options.builddir == 'meson.build' else options.builddir[:-11] + list_projinfo_from_source(sourcedir) + return 0 if not os.path.isdir(datadir): print('Current directory is not a build dir. Please specify it or ' 'change the working directory to it.') diff --git a/mesonbuild/rewriter.py b/mesonbuild/rewriter.py index 5da8c8903..37ed7efd6 100644 --- a/mesonbuild/rewriter.py +++ b/mesonbuild/rewriter.py @@ -41,7 +41,7 @@ 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.AstInterpreter(options.sourcedir, '') + rewriter = mesonbuild.astinterpreter.RewriterInterpreter(options.sourcedir, '') try: if options.commands[0] == 'add': rewriter.add_source(options.target, options.filename) diff --git a/run_unittests.py b/run_unittests.py index 8351dadb7..d23918cdc 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -1165,6 +1165,13 @@ class BasePlatformTests(unittest.TestCase): universal_newlines=True) return json.loads(out) + def introspect_directory(self, directory, args): + if isinstance(args, str): + args = [args] + out = subprocess.check_output(self.mintro_command + args + [directory], + universal_newlines=True) + return json.loads(out) + def assertPathEqual(self, path1, path2): ''' Handles a lot of platform-specific quirks related to paths such as @@ -2913,6 +2920,33 @@ recommended as it is not supported on some platforms''') 'target2-id', '@other') self.assertEqual('81d46d1@@target2-id@other', target_id) + def test_introspect_projectinfo_without_configured_build(self): + testfile = os.path.join(self.common_test_dir, '36 run program', 'meson.build') + res = self.introspect_directory(testfile, '--projectinfo') + self.assertEqual(set(res['buildsystem_files']), set(['meson.build'])) + self.assertEqual(res['version'], None) + self.assertEqual(res['descriptive_name'], 'run command') + self.assertEqual(res['subprojects'], []) + + testfile = os.path.join(self.common_test_dir, '44 options', 'meson.build') + res = self.introspect_directory(testfile, '--projectinfo') + self.assertEqual(set(res['buildsystem_files']), set(['meson_options.txt', 'meson.build'])) + self.assertEqual(res['version'], None) + self.assertEqual(res['descriptive_name'], 'options') + self.assertEqual(res['subprojects'], []) + + testfile = os.path.join(self.common_test_dir, '47 subproject options', 'meson.build') + res = self.introspect_directory(testfile, '--projectinfo') + self.assertEqual(set(res['buildsystem_files']), set(['meson_options.txt', 'meson.build'])) + self.assertEqual(res['version'], None) + self.assertEqual(res['descriptive_name'], 'suboptions') + self.assertEqual(len(res['subprojects']), 1) + subproject_files = set(f.replace('\\', '/') for f in res['subprojects'][0]['buildsystem_files']) + self.assertEqual(subproject_files, set(['subprojects/subproject/meson_options.txt', 'subprojects/subproject/meson.build'])) + self.assertEqual(res['subprojects'][0]['name'], 'subproject') + self.assertEqual(res['subprojects'][0]['version'], 'undefined') + self.assertEqual(res['subprojects'][0]['descriptive_name'], 'subproject') + class FailureTests(BasePlatformTests): ''' |