Merge pull request #4191 from trilader/feature/projectinfo-from-source
mintro: Allow introspect --projectinfo without build directory
This commit is contained in:
commit
270f2395d6
|
@ -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.
|
|
@ -18,7 +18,7 @@
|
||||||
from . import interpreterbase, mlog, mparser, mesonlib
|
from . import interpreterbase, mlog, mparser, mesonlib
|
||||||
from . import environment
|
from . import environment
|
||||||
|
|
||||||
from .interpreterbase import InterpreterException, InvalidArguments
|
from .interpreterbase import InterpreterException, InvalidArguments, BreakRequest, ContinueRequest
|
||||||
|
|
||||||
import os, sys
|
import os, sys
|
||||||
|
|
||||||
|
@ -46,7 +46,6 @@ REMOVE_SOURCE = 1
|
||||||
class AstInterpreter(interpreterbase.InterpreterBase):
|
class AstInterpreter(interpreterbase.InterpreterBase):
|
||||||
def __init__(self, source_root, subdir):
|
def __init__(self, source_root, subdir):
|
||||||
super().__init__(source_root, subdir)
|
super().__init__(source_root, subdir)
|
||||||
self.asts = {}
|
|
||||||
self.funcs.update({'project': self.func_do_nothing,
|
self.funcs.update({'project': self.func_do_nothing,
|
||||||
'test': self.func_do_nothing,
|
'test': self.func_do_nothing,
|
||||||
'benchmark': self.func_do_nothing,
|
'benchmark': self.func_do_nothing,
|
||||||
|
@ -76,7 +75,72 @@ class AstInterpreter(interpreterbase.InterpreterBase):
|
||||||
'vcs_tag': self.func_do_nothing,
|
'vcs_tag': self.func_do_nothing,
|
||||||
'add_languages': self.func_do_nothing,
|
'add_languages': self.func_do_nothing,
|
||||||
'declare_dependency': 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,
|
'executable': self.func_executable,
|
||||||
'static_library': self.func_static_lib,
|
'static_library': self.func_static_lib,
|
||||||
'shared_library': self.func_shared_lib,
|
'shared_library': self.func_shared_lib,
|
||||||
|
@ -90,12 +154,6 @@ class AstInterpreter(interpreterbase.InterpreterBase):
|
||||||
'is_variable': self.func_is_variable,
|
'is_variable': self.func_is_variable,
|
||||||
})
|
})
|
||||||
|
|
||||||
def func_do_nothing(self, node, args, kwargs):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def method_call(self, node):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def func_executable(self, node, args, kwargs):
|
def func_executable(self, node, args, kwargs):
|
||||||
if args[0] == self.targetname:
|
if args[0] == self.targetname:
|
||||||
if self.operation == ADD_SOURCE:
|
if self.operation == ADD_SOURCE:
|
||||||
|
@ -147,21 +205,6 @@ class AstInterpreter(interpreterbase.InterpreterBase):
|
||||||
return [args]
|
return [args]
|
||||||
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):
|
def transform(self):
|
||||||
self.load_root_meson_file()
|
self.load_root_meson_file()
|
||||||
self.asts[''] = self.ast
|
self.asts[''] = self.ast
|
||||||
|
@ -181,9 +224,6 @@ class AstInterpreter(interpreterbase.InterpreterBase):
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
self.transform()
|
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):
|
def add_source_to_target(self, node, args, kwargs):
|
||||||
namespan = node.args.arguments[0].bytespan
|
namespan = node.args.arguments[0].bytespan
|
||||||
buildfilename = os.path.join(self.source_root, self.subdir, environment.build_filename)
|
buildfilename = os.path.join(self.source_root, self.subdir, environment.build_filename)
|
||||||
|
|
|
@ -22,6 +22,9 @@ project files and don't need this info."""
|
||||||
import json
|
import json
|
||||||
from . import build, mtest, coredata as cdata
|
from . import build, mtest, coredata as cdata
|
||||||
from . import mesonlib
|
from . import mesonlib
|
||||||
|
from . import astinterpreter
|
||||||
|
from . import mparser
|
||||||
|
from .interpreterbase import InvalidArguments
|
||||||
from .backend import ninjabackend
|
from .backend import ninjabackend
|
||||||
import sys, os
|
import sys, os
|
||||||
import pathlib
|
import pathlib
|
||||||
|
@ -178,14 +181,18 @@ def add_keys(optlist, options, section):
|
||||||
optdict['description'] = opt.description
|
optdict['description'] = opt.description
|
||||||
optlist.append(optdict)
|
optlist.append(optdict)
|
||||||
|
|
||||||
def list_buildsystem_files(builddata):
|
def find_buildsystem_files_list(src_dir):
|
||||||
src_dir = builddata.environment.get_source_dir()
|
|
||||||
# I feel dirty about this. But only slightly.
|
# I feel dirty about this. But only slightly.
|
||||||
filelist = []
|
filelist = []
|
||||||
for root, _, files in os.walk(src_dir):
|
for root, _, files in os.walk(src_dir):
|
||||||
for f in files:
|
for f in files:
|
||||||
if f == 'meson.build' or f == 'meson_options.txt':
|
if f == 'meson.build' or f == 'meson_options.txt':
|
||||||
filelist.append(os.path.relpath(os.path.join(root, f), src_dir))
|
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))
|
print(json.dumps(filelist))
|
||||||
|
|
||||||
def list_deps(coredata):
|
def list_deps(coredata):
|
||||||
|
@ -219,19 +226,81 @@ def list_tests(testdata):
|
||||||
print(json.dumps(result))
|
print(json.dumps(result))
|
||||||
|
|
||||||
def list_projinfo(builddata):
|
def list_projinfo(builddata):
|
||||||
result = {'name': builddata.project_name, 'version': builddata.project_version}
|
result = {'version': builddata.project_version,
|
||||||
|
'descriptive_name': builddata.project_name}
|
||||||
subprojects = []
|
subprojects = []
|
||||||
for k, v in builddata.subprojects.items():
|
for k, v in builddata.subprojects.items():
|
||||||
c = {'name': k,
|
c = {'name': k,
|
||||||
'version': v}
|
'version': v,
|
||||||
|
'descriptive_name': builddata.projects.get(k)}
|
||||||
subprojects.append(c)
|
subprojects.append(c)
|
||||||
result['subprojects'] = subprojects
|
result['subprojects'] = subprojects
|
||||||
print(json.dumps(result))
|
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):
|
def run(options):
|
||||||
datadir = 'meson-private'
|
datadir = 'meson-private'
|
||||||
if options.builddir is not None:
|
if options.builddir is not None:
|
||||||
datadir = os.path.join(options.builddir, datadir)
|
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):
|
if not os.path.isdir(datadir):
|
||||||
print('Current directory is not a build dir. Please specify it or '
|
print('Current directory is not a build dir. Please specify it or '
|
||||||
'change the working directory to it.')
|
'change the working directory to it.')
|
||||||
|
|
|
@ -41,7 +41,7 @@ def run(options):
|
||||||
if options.target is None or options.filename is None:
|
if options.target is None or options.filename is None:
|
||||||
sys.exit("Must specify both target and filename.")
|
sys.exit("Must specify both target and filename.")
|
||||||
print('This tool is highly experimental, use with care.')
|
print('This tool is highly experimental, use with care.')
|
||||||
rewriter = mesonbuild.astinterpreter.AstInterpreter(options.sourcedir, '')
|
rewriter = mesonbuild.astinterpreter.RewriterInterpreter(options.sourcedir, '')
|
||||||
try:
|
try:
|
||||||
if options.commands[0] == 'add':
|
if options.commands[0] == 'add':
|
||||||
rewriter.add_source(options.target, options.filename)
|
rewriter.add_source(options.target, options.filename)
|
||||||
|
|
|
@ -1165,6 +1165,13 @@ class BasePlatformTests(unittest.TestCase):
|
||||||
universal_newlines=True)
|
universal_newlines=True)
|
||||||
return json.loads(out)
|
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):
|
def assertPathEqual(self, path1, path2):
|
||||||
'''
|
'''
|
||||||
Handles a lot of platform-specific quirks related to paths such as
|
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')
|
'target2-id', '@other')
|
||||||
self.assertEqual('81d46d1@@target2-id@other', target_id)
|
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):
|
class FailureTests(BasePlatformTests):
|
||||||
'''
|
'''
|
||||||
|
|
Loading…
Reference in New Issue