diff options
-rw-r--r-- | docs/markdown/IDE-integration.md | 55 | ||||
-rw-r--r-- | docs/markdown/snippets/introspect_deps_no_bd.md | 25 | ||||
-rw-r--r-- | docs/markdown/snippets/introspect_targets_no_bd.md | 21 | ||||
-rw-r--r-- | mesonbuild/ast/__init__.py | 3 | ||||
-rw-r--r-- | mesonbuild/ast/introspection.py | 14 | ||||
-rw-r--r-- | mesonbuild/ast/postprocess.py | 30 | ||||
-rw-r--r-- | mesonbuild/mintro.py | 148 | ||||
-rw-r--r-- | mesonbuild/rewriter.py | 4 | ||||
-rwxr-xr-x | run_unittests.py | 64 | ||||
-rw-r--r-- | test cases/unit/52 introspection/meson.build | 6 |
10 files changed, 323 insertions, 47 deletions
diff --git a/docs/markdown/IDE-integration.md b/docs/markdown/IDE-integration.md index ab157d1..7bbec5d 100644 --- a/docs/markdown/IDE-integration.md +++ b/docs/markdown/IDE-integration.md @@ -109,6 +109,29 @@ The following table shows all valid types for a target. `run` | A Meson run target `jar` | A Java JAR target +### Using `--targets` without a build directory + +It is also possible to get most targets without a build directory. This can be +done by running `meson introspect --targets /path/to/meson.build`. + +The generated output is similar to running the introspection with a build +directory or reading the `intro-targets.json`. However, there are some key +differences: + +- The paths in `filename` now are _relative_ to the future build directory +- The `install_filename` key is completely missing +- There is only one entry in `target_sources`: + - With the language set to `unknown` + - Empty lists for `compiler` and `parameters` and `generated_sources` + - The `sources` list _should_ contain all sources of the target + +There is no guarantee that the sources list in `target_sources` is correct. +There might be differences, due to internal limitations. It is also not +guaranteed that all targets will be listed in the output. It might even be +possible that targets are listed, which won't exist when meson is run normally. +This can happen if a target is defined inside an if statement. +Use this feature with care. + ## Build Options The list of all build options (build type, warning level, etc.) is stored in @@ -158,6 +181,38 @@ However, this behavior is not guaranteed if subprojects are present. Due to internal limitations all subprojects are processed even if they are never used in a real meson run. Because of this options for the subprojects can differ. +## The dependencies section + +The list of all _found_ dependencies can be acquired from +`intro-dependencies.json`. Here, the name, compiler and linker arguments for +a dependency are listed. + +### Scanning for dependecie with `--scan-dependencies` + +It is also possible to get most dependencies used without a build directory. +This can be done by running `meson introspect --scan-dependencies /path/to/meson.build`. + +The output format is as follows: + +```json +[ + { + "name": "The name of the dependency", + "required": true, + "conditional": false, + "has_fallback": false + } +] +``` + +The `required` keyword specifies whether the dependency is marked as required +in the `meson.build` (all dependencies are required by default). The +`conditional` key indicates whether the `dependency()` function was called +inside a conditional block. In a real meson run these dependencies might not be +used, thus they _may_ not be required, even if the `required` key is set. The +`has_fallback` key just indicates whether a fallback was directly set in the +`dependency()` function. + ## Tests Compilation and unit tests are done as usual by running the `ninja` and diff --git a/docs/markdown/snippets/introspect_deps_no_bd.md b/docs/markdown/snippets/introspect_deps_no_bd.md new file mode 100644 index 0000000..cfae58b --- /dev/null +++ b/docs/markdown/snippets/introspect_deps_no_bd.md @@ -0,0 +1,25 @@ +## `introspect --scan-dependencies` can now be used to scan for dependencies used in a project + +It is now possible to run `meson introspect --scan-dependencies /path/to/meson.build` +without a configured build directory to scan for dependencies. + +The output format is as follows: + +```json +[ + { + "name": "The name of the dependency", + "required": true, + "conditional": false, + "has_fallback": false + } +] +``` + +The `required` keyword specifies whether the dependency is marked as required +in the `meson.build` (all dependencies are required by default). The +`conditional` key indicates whether the `dependency()` function was called +inside a conditional block. In a real meson run these dependencies might not be +used, thus they _may_ not be required, even if the `required` key is set. The +`has_fallback` key just indicates whether a fallback was directly set in the +`dependency()` function. diff --git a/docs/markdown/snippets/introspect_targets_no_bd.md b/docs/markdown/snippets/introspect_targets_no_bd.md new file mode 100644 index 0000000..0172a4e --- /dev/null +++ b/docs/markdown/snippets/introspect_targets_no_bd.md @@ -0,0 +1,21 @@ +## `introspect --targets` can now be used without configured build directory + +It is now possible to run `meson introspect --targets /path/to/meson.build` +without a configured build directory. + +The generated output is similar to running the introspection with a build +directory. However, there are some key differences: + +- The paths in `filename` now are _relative_ to the future build directory +- The `install_filename` key is completely missing +- There is only one entry in `target_sources`: + - With the language set to `unknown` + - Empty lists for `compiler` and `parameters` and `generated_sources` + - The `sources` list _should_ contain all sources of the target + +There is no guarantee that the sources list in `target_sources` is correct. +There might be differences, due to internal limitations. It is also not +guaranteed that all targets will be listed in the output. It might even be +possible that targets are listed, which won't exist when meson is run normally. +This can happen if a target is defined inside an if statement. +Use this feature with care.
\ No newline at end of file diff --git a/mesonbuild/ast/__init__.py b/mesonbuild/ast/__init__.py index a9370dc..48de523 100644 --- a/mesonbuild/ast/__init__.py +++ b/mesonbuild/ast/__init__.py @@ -16,6 +16,7 @@ # or an interpreter-based tool. __all__ = [ + 'AstConditionLevel', 'AstInterpreter', 'AstIDGenerator', 'AstIndentationGenerator', @@ -28,5 +29,5 @@ __all__ = [ from .interpreter import AstInterpreter from .introspection import IntrospectionInterpreter, build_target_functions from .visitor import AstVisitor -from .postprocess import AstIDGenerator, AstIndentationGenerator +from .postprocess import AstConditionLevel, AstIDGenerator, AstIndentationGenerator from .printer import AstPrinter diff --git a/mesonbuild/ast/introspection.py b/mesonbuild/ast/introspection.py index 6ac5929..12cb379 100644 --- a/mesonbuild/ast/introspection.py +++ b/mesonbuild/ast/introspection.py @@ -137,8 +137,16 @@ class IntrospectionInterpreter(AstInterpreter): if not args: return name = args[0] + has_fallback = 'fallback' in kwargs + required = kwargs.get('required', True) + condition_level = node.condition_level if hasattr(node, 'condition_level') else 0 + if isinstance(required, ElementaryNode): + required = required.value self.dependencies += [{ 'name': name, + 'required': required, + 'has_fallback': has_fallback, + 'conditional': condition_level > 0, 'node': node }] @@ -180,11 +188,11 @@ class IntrospectionInterpreter(AstInterpreter): source_nodes += [curr] # Make sure nothing can crash when creating the build class - kwargs = {} + kwargs_reduced = {k: v for k, v in kwargs.items() if k in targetclass.known_kwargs and k in ['install', 'build_by_default', 'build_always']} 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) + target = targetclass(name, self.subdir, self.subproject, is_cross, empty_sources, objects, self.environment, kwargs_reduced) self.targets += [{ 'name': target.get_basename(), @@ -193,6 +201,8 @@ class IntrospectionInterpreter(AstInterpreter): '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, + 'installed': target.should_install(), + 'outputs': target.get_outputs(), 'sources': source_nodes, 'kwargs': kwargs, 'node': node, diff --git a/mesonbuild/ast/postprocess.py b/mesonbuild/ast/postprocess.py index e913b4f..8e8732f 100644 --- a/mesonbuild/ast/postprocess.py +++ b/mesonbuild/ast/postprocess.py @@ -84,3 +84,33 @@ class AstIDGenerator(AstVisitor): self.counter[name] = 0 node.ast_id = name + '#' + str(self.counter[name]) self.counter[name] += 1 + +class AstConditionLevel(AstVisitor): + def __init__(self): + self.condition_level = 0 + + def visit_default_func(self, node: mparser.BaseNode): + node.condition_level = self.condition_level + + def visit_ForeachClauseNode(self, node: mparser.ForeachClauseNode): + self.visit_default_func(node) + self.condition_level += 1 + node.items.accept(self) + node.block.accept(self) + self.condition_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.condition_level += 1 + node.elseblock.accept(self) + self.condition_level -= 1 + + def visit_IfNode(self, node: mparser.IfNode): + self.visit_default_func(node) + self.condition_level += 1 + node.condition.accept(self) + node.block.accept(self) + self.condition_level -= 1 diff --git a/mesonbuild/mintro.py b/mesonbuild/mintro.py index b1e6509..243dc5d 100644 --- a/mesonbuild/mintro.py +++ b/mesonbuild/mintro.py @@ -22,9 +22,11 @@ project files and don't need this info.""" import json from . import build, coredata as cdata from . import mesonlib -from .ast import IntrospectionInterpreter +from .ast import IntrospectionInterpreter, build_target_functions, AstConditionLevel, AstIDGenerator, AstIndentationGenerator from . import mlog from .backend import backends +from .mparser import FunctionNode, ArrayNode, ArgumentNode, StringNode +from typing import List, Optional import sys, os import pathlib @@ -37,7 +39,10 @@ def get_meson_introspection_version(): def get_meson_introspection_required_version(): return ['>=1.0', '<2.0'] -def get_meson_introspection_types(coredata: cdata.CoreData = None, builddata: build.Build = None, backend: backends.Backend = None): +def get_meson_introspection_types(coredata: Optional[cdata.CoreData] = None, + builddata: Optional[build.Build] = None, + backend: Optional[backends.Backend] = None, + sourcedir: Optional[str] = None): if backend and builddata: benchmarkdata = backend.create_test_serialisation(builddata.get_benchmarks()) testdata = backend.create_test_serialisation(builddata.get_tests()) @@ -52,6 +57,7 @@ def get_meson_introspection_types(coredata: cdata.CoreData = None, builddata: bu }, 'buildoptions': { 'func': lambda: list_buildoptions(coredata), + 'no_bd': lambda intr: list_buildoptions_from_source(intr), 'desc': 'List all build options.', }, 'buildsystem_files': { @@ -61,18 +67,26 @@ def get_meson_introspection_types(coredata: cdata.CoreData = None, builddata: bu }, 'dependencies': { 'func': lambda: list_deps(coredata), + 'no_bd': lambda intr: list_deps_from_source(intr), 'desc': 'List external dependencies.', }, + 'scan_dependencies': { + 'no_bd': lambda intr: list_deps_from_source(intr), + 'desc': 'Scan for dependencies used in the meson.build file.', + 'key': 'scan-dependencies', + }, 'installed': { 'func': lambda: list_installed(installdata), 'desc': 'List all installed files and directories.', }, 'projectinfo': { 'func': lambda: list_projinfo(builddata), + 'no_bd': lambda intr: list_projinfo_from_source(sourcedir, intr), 'desc': 'Information about projects.', }, 'targets': { 'func': lambda: list_targets(builddata, installdata, backend), + 'no_bd': lambda intr: list_targets_from_source(intr), 'desc': 'List top level targets.', }, 'tests': { @@ -113,6 +127,46 @@ def list_installed(installdata): res[path] = os.path.join(installdata.prefix, installpath) return res +def list_targets_from_source(intr: IntrospectionInterpreter): + tlist = [] + for i in intr.targets: + sources = [] + for n in i['sources']: + args = [] + if isinstance(n, FunctionNode): + args = list(n.args.arguments) + if n.func_name in build_target_functions: + args.pop(0) + elif isinstance(n, ArrayNode): + args = n.args.arguments + elif isinstance(n, ArgumentNode): + args = n.arguments + for j in args: + if isinstance(j, StringNode): + sources += [j.value] + elif isinstance(j, str): + sources += [j] + + tlist += [{ + 'name': i['name'], + 'id': i['id'], + 'type': i['type'], + 'defined_in': i['defined_in'], + 'filename': [os.path.join(i['subdir'], x) for x in i['outputs']], + 'build_by_default': i['build_by_default'], + 'target_sources': [{ + 'language': 'unknown', + 'compiler': [], + 'parameters': [], + 'sources': [os.path.normpath(os.path.join(os.path.abspath(intr.source_root), i['subdir'], x)) for x in sources], + 'generated_sources': [] + }], + 'subproject': None, # Subprojects are not supported + 'installed': i['installed'] + }] + + return tlist + def list_targets(builddata: build.Build, installdata, backend: backends.Backend): tlist = [] build_dir = builddata.environment.get_build_dir() @@ -147,15 +201,8 @@ def list_targets(builddata: build.Build, installdata, backend: backends.Backend) tlist.append(t) return tlist -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() - backend = backends.get_backend_from_name(backend, None) - intr = IntrospectionInterpreter(sourcedir, '', backend.name) - intr.analyze() - # Reenable logging just in case - mlog.enable() - print(json.dumps(list_buildoptions(intr.coredata), indent=indent)) +def list_buildoptions_from_source(intr: IntrospectionInterpreter) -> List[dict]: + return list_buildoptions(intr.coredata) def list_target_files(target_name: str, targets: list, source_dir: str): sys.stderr.write("WARNING: The --target-files introspection API is deprecated. Use --targets instead.\n") @@ -178,7 +225,7 @@ def list_target_files(target_name: str, targets: list, source_dir: str): return result -def list_buildoptions(coredata: cdata.CoreData): +def list_buildoptions(coredata: cdata.CoreData) -> List[dict]: optlist = [] dir_option_names = ['bindir', @@ -250,6 +297,12 @@ def list_buildsystem_files(builddata: build.Build): filelist = [os.path.join(src_dir, x) for x in filelist] return filelist +def list_deps_from_source(intr: IntrospectionInterpreter): + result = [] + for i in intr.dependencies: + result += [{k: v for k, v in i.items() if k in ['name', 'required', 'has_fallback', 'conditional']}] + return result + def list_deps(coredata: cdata.CoreData): result = [] for d in coredata.deps.values(): @@ -299,15 +352,10 @@ def list_projinfo(builddata: build.Build): result['subprojects'] = subprojects return result -def list_projinfo_from_source(sourcedir, indent): +def list_projinfo_from_source(sourcedir: str, intr: IntrospectionInterpreter): files = find_buildsystem_files_list(sourcedir) files = [os.path.normpath(x) for x in files] - mlog.disable() - intr = IntrospectionInterpreter(sourcedir, '', 'ninja') - intr.analyze() - mlog.enable() - for i in intr.project_data['subprojects']: basedir = os.path.join(intr.subproject_dir, i['name']) i['buildsystem_files'] = [x for x in files if x.startswith(basedir)] @@ -315,23 +363,47 @@ def list_projinfo_from_source(sourcedir, indent): intr.project_data['buildsystem_files'] = files intr.project_data['subproject_dir'] = intr.subproject_dir - print(json.dumps(intr.project_data, indent=indent)) + return intr.project_data + +def print_results(options, results, indent): + if len(results) == 0 and not options.force_dict: + print('No command specified') + return 1 + elif len(results) == 1 and not options.force_dict: + # Make to keep the existing output format for a single option + print(json.dumps(results[0][1], indent=indent)) + else: + out = {} + for i in results: + out[i[0]] = i[1] + print(json.dumps(out, indent=indent)) + return 0 def run(options): datadir = 'meson-private' infodir = 'meson-info' - indent = 4 if options.indent else None if options.builddir is not None: datadir = os.path.join(options.builddir, datadir) infodir = os.path.join(options.builddir, infodir) + indent = 4 if options.indent else None + results = [] + sourcedir = '.' if options.builddir == 'meson.build' else options.builddir[:-11] + intro_types = get_meson_introspection_types(sourcedir=sourcedir) + if 'meson.build' in [os.path.basename(options.builddir), options.builddir]: - sourcedir = '.' if options.builddir == 'meson.build' else options.builddir[:-11] - if options.projectinfo: - list_projinfo_from_source(sourcedir, indent) - return 0 - if options.buildoptions: - list_buildoptions_from_source(sourcedir, options.backend, indent) - return 0 + # Make sure that log entries in other parts of meson don't interfere with the JSON output + mlog.disable() + backend = backends.get_backend_from_name(options.backend, None) + intr = IntrospectionInterpreter(sourcedir, '', backend.name, visitors = [AstIDGenerator(), AstIndentationGenerator(), AstConditionLevel()]) + intr.analyze() + # Reenable logging just in case + mlog.enable() + for key, val in intro_types.items(): + if (not options.all and not getattr(options, key, False)) or 'no_bd' not in val: + continue + results += [(key, val['no_bd'](intr))] + return print_results(options, results, indent) + infofile = get_meson_info_file(infodir) if not os.path.isdir(datadir) or not os.path.isdir(infodir) or not os.path.isfile(infofile): print('Current directory is not a meson build directory.' @@ -355,9 +427,6 @@ def run(options): .format(intro_vers, ' and '.join(vers_to_check))) return 1 - results = [] - intro_types = get_meson_introspection_types() - # Handle the one option that does not have its own JSON file (meybe deprecate / remove this?) if options.target_files is not None: targets_file = os.path.join(infodir, 'intro-targets.json') @@ -367,6 +436,8 @@ def run(options): # Extract introspection information from JSON for i in intro_types.keys(): + if 'func' not in intro_types[i]: + continue if not options.all and not getattr(options, i, False): continue curr = os.path.join(infodir, 'intro-{}.json'.format(i)) @@ -376,18 +447,7 @@ def run(options): with open(curr, 'r') as fp: results += [(i, json.load(fp))] - if len(results) == 0 and not options.force_dict: - print('No command specified') - return 1 - elif len(results) == 1 and not options.force_dict: - # Make to keep the existing output format for a single option - print(json.dumps(results[0][1], indent=indent)) - else: - out = {} - for i in results: - out[i[0]] = i[1] - print(json.dumps(out, indent=indent)) - return 0 + return print_results(options, results, indent) updated_introspection_files = [] @@ -408,6 +468,8 @@ def generate_introspection_file(builddata: build.Build, backend: backends.Backen intro_info = [] for key, val in intro_types.items(): + if 'func' not in val: + continue intro_info += [(key, val['func']())] write_intro_info(intro_info, builddata.environment.info_dir) @@ -436,6 +498,8 @@ def write_meson_info_file(builddata: build.Build, errors: list, build_files_upda intro_info = {} for i in intro_types.keys(): + if 'func' not in intro_types[i]: + continue intro_info[i] = { 'file': 'intro-{}.json'.format(i), 'updated': i in updated_introspection_files diff --git a/mesonbuild/rewriter.py b/mesonbuild/rewriter.py index ec78521..c997434 100644 --- a/mesonbuild/rewriter.py +++ b/mesonbuild/rewriter.py @@ -23,7 +23,7 @@ # - move targets # - reindent? -from .ast import IntrospectionInterpreter, build_target_functions, AstIDGenerator, AstIndentationGenerator, AstPrinter +from .ast import IntrospectionInterpreter, build_target_functions, AstConditionLevel, AstIDGenerator, AstIndentationGenerator, AstPrinter from mesonbuild.mesonlib import MesonException from . import mlog, mparser, environment from functools import wraps @@ -324,7 +324,7 @@ rewriter_func_kwargs = { class Rewriter: def __init__(self, sourcedir: str, generator: str = 'ninja'): self.sourcedir = sourcedir - self.interpreter = IntrospectionInterpreter(sourcedir, '', generator, visitors = [AstIDGenerator(), AstIndentationGenerator()]) + self.interpreter = IntrospectionInterpreter(sourcedir, '', generator, visitors = [AstIDGenerator(), AstIndentationGenerator(), AstConditionLevel()]) self.modefied_nodes = [] self.to_remove_nodes = [] self.to_add_nodes = [] diff --git a/run_unittests.py b/run_unittests.py index b5cb53c..c1337a3 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -3511,6 +3511,70 @@ recommended as it is not supported on some platforms''') self.assertListEqual(res1, res2) + def test_introspect_targets_from_source(self): + testdir = os.path.join(self.unit_test_dir, '52 introspection') + testfile = os.path.join(testdir, 'meson.build') + introfile = os.path.join(self.builddir, 'meson-info', 'intro-targets.json') + self.init(testdir) + self.assertPathExists(introfile) + with open(introfile, 'r') as fp: + res_wb = json.load(fp) + + res_nb = self.introspect_directory(testfile, ['--targets'] + self.meson_args) + + # Account for differences in output + for i in res_wb: + i['filename'] = [os.path.relpath(x, self.builddir) for x in i['filename']] + if 'install_filename' in i: + del i['install_filename'] + + sources = [] + for j in i['target_sources']: + sources += j['sources'] + i['target_sources'] = [{ + 'language': 'unknown', + 'compiler': [], + 'parameters': [], + 'sources': sources, + 'generated_sources': [] + }] + + self.maxDiff = None + self.assertListEqual(res_nb, res_wb) + + def test_introspect_dependencies_from_source(self): + testdir = os.path.join(self.unit_test_dir, '52 introspection') + testfile = os.path.join(testdir, 'meson.build') + res_nb = self.introspect_directory(testfile, ['--scan-dependencies'] + self.meson_args) + expected = [ + { + 'name': 'threads', + 'required': True, + 'has_fallback': False, + 'conditional': False + }, + { + 'name': 'zlib', + 'required': False, + 'has_fallback': False, + 'conditional': False + }, + { + 'name': 'somethingthatdoesnotexist', + 'required': True, + 'has_fallback': False, + 'conditional': True + }, + { + 'name': 'look_i_have_a_fallback', + 'required': True, + 'has_fallback': True, + 'conditional': True + } + ] + self.maxDiff = None + self.assertListEqual(res_nb, expected) + class FailureTests(BasePlatformTests): ''' Tests that test failure conditions. Build files here should be dynamically diff --git a/test cases/unit/52 introspection/meson.build b/test cases/unit/52 introspection/meson.build index 14d880b..98f6f22 100644 --- a/test cases/unit/52 introspection/meson.build +++ b/test cases/unit/52 introspection/meson.build @@ -1,6 +1,12 @@ project('introspection', ['c', 'cpp'], version: '1.2.3', default_options: ['cpp_std=c++11', 'buildtype=debug']) dep1 = dependency('threads') +dep2 = dependency('zlib', required: false) + +if false + dependency('somethingthatdoesnotexist', required: true) + dependency('look_i_have_a_fallback', fallback: ['oh_no', 'the_subproject_does_not_exist']) +endif subdir('sharedlib') subdir('staticlib') |