diff --git a/docs/markdown/snippets/introspect_meson_info.md b/docs/markdown/snippets/introspect_meson_info.md new file mode 100644 index 000000000..42f2fda8d --- /dev/null +++ b/docs/markdown/snippets/introspect_meson_info.md @@ -0,0 +1,6 @@ +## Added the `meson-info.json` introspection file + +Meson now generates a `meson-info.json` file in the `meson-info` directory +to provide introspection information about the latest meson run. This file +is updated when the build configuration is changed and the build files are +(re)generated. diff --git a/mesonbuild/mconf.py b/mesonbuild/mconf.py index 2863b0c0b..b8fb3c683 100644 --- a/mesonbuild/mconf.py +++ b/mesonbuild/mconf.py @@ -148,6 +148,7 @@ class Conf: def run(options): coredata.parse_cmd_line_options(options) builddir = os.path.abspath(os.path.realpath(options.builddir)) + c = None try: c = Conf(builddir) save = False @@ -163,7 +164,10 @@ def run(options): if save: c.save() mintro.update_build_options(c.coredata, c.build.environment.info_dir) + mintro.write_meson_info_file(c.build, []) except ConfException as e: print('Meson configurator encountered an error:') + if c is not None and c.build is not None: + mintro.write_meson_info_file(c.build, [e]) raise e return 0 diff --git a/mesonbuild/mintro.py b/mesonbuild/mintro.py index 3382e0da0..b183d2ad3 100644 --- a/mesonbuild/mintro.py +++ b/mesonbuild/mintro.py @@ -33,25 +33,64 @@ from .backend import backends import sys, os import pathlib +def get_meson_info_file(info_dir: str): + return os.path.join(info_dir, 'meson-info.json') + +def get_meson_introspection_version(): + return '1.0.0' + +def get_meson_introspection_types(coredata: cdata.CoreData = None, builddata: build.Build = None, backend: backends.Backend = None): + if backend and builddata: + benchmarkdata = backend.create_test_serialisation(builddata.get_benchmarks()) + testdata = backend.create_test_serialisation(builddata.get_tests()) + installdata = backend.create_install_data() + else: + benchmarkdata = testdata = installdata = None + + return { + 'benchmarks': { + 'func': lambda: list_benchmarks(benchmarkdata), + 'desc': 'List all benchmarks.', + }, + 'buildoptions': { + 'func': lambda: list_buildoptions(coredata), + 'desc': 'List all build options.', + }, + 'buildsystem_files': { + 'func': lambda: list_buildsystem_files(builddata), + 'desc': 'List files that make up the build system.', + 'key': 'buildsystem-files', + }, + 'dependencies': { + 'func': lambda: list_deps(coredata), + 'desc': 'List external dependencies.', + }, + 'installed': { + 'func': lambda: list_installed(installdata), + 'desc': 'List all installed files and directories.', + }, + 'projectinfo': { + 'func': lambda: list_projinfo(builddata), + 'desc': 'Information about projects.', + }, + 'targets': { + 'func': lambda: list_targets(builddata, installdata, backend), + 'desc': 'List top level targets.', + }, + 'tests': { + 'func': lambda: list_tests(testdata), + 'desc': 'List all unit tests.', + } + } + def add_arguments(parser): - parser.add_argument('--targets', action='store_true', dest='list_targets', default=False, - help='List top level targets.') - parser.add_argument('--installed', action='store_true', dest='list_installed', default=False, - help='List all installed files and directories.') + intro_types = get_meson_introspection_types() + for key, val in intro_types.items(): + flag = '--' + val.get('key', key) + parser.add_argument(flag, action='store_true', dest=key, default=False, help=val['desc']) + parser.add_argument('--target-files', action='store', dest='target_files', default=None, help='List source files for a given target.') - parser.add_argument('--buildsystem-files', action='store_true', dest='buildsystem_files', default=False, - help='List files that make up the build system.') - parser.add_argument('--buildoptions', action='store_true', dest='buildoptions', default=False, - help='List all build options.') - parser.add_argument('--tests', action='store_true', dest='tests', default=False, - help='List all unit tests.') - parser.add_argument('--benchmarks', action='store_true', dest='benchmarks', default=False, - help='List all benchmarks.') - parser.add_argument('--dependencies', action='store_true', dest='dependencies', default=False, - help='List external dependencies.') - parser.add_argument('--projectinfo', action='store_true', dest='projectinfo', default=False, - help='Information about projects.') parser.add_argument('--backend', choices=cdata.backendlist, dest='backend', default='ninja', help='The backend to use for the --buildoptions introspection.') parser.add_argument('-a', '--all', action='store_true', dest='all', default=False, @@ -74,7 +113,7 @@ def list_installed(installdata): res[path] = os.path.join(installdata.prefix, installdir, os.path.basename(path)) for path, installpath, unused_custom_install_mode in installdata.man: res[path] = os.path.join(installdata.prefix, installpath) - return ('installed', res) + return res def list_targets(builddata: build.Build, installdata, backend: backends.Backend): tlist = [] @@ -110,7 +149,7 @@ def list_targets(builddata: build.Build, installdata, backend: backends.Backend) else: t['installed'] = False tlist.append(t) - return ('targets', tlist) + return tlist class BuildoptionsOptionHelper: # mimic an argparse namespace @@ -257,8 +296,7 @@ def list_buildoptions_from_source(sourcedir, backend, indent): intr.analyze() # Reenable logging just in case mlog.enable() - buildoptions = list_buildoptions(intr.coredata)[1] - print(json.dumps(buildoptions, indent=indent)) + print(json.dumps(list_buildoptions(intr.coredata), indent=indent)) def list_target_files(target_name, targets, builddata: build.Build): result = [] @@ -279,7 +317,7 @@ def list_target_files(target_name, targets, builddata: build.Build): # TODO Remove this line in a future PR with other breaking changes result = list(map(lambda x: os.path.relpath(x, builddata.environment.get_source_dir()), result)) - return ('target_files', result) + return result def list_buildoptions(coredata: cdata.CoreData): optlist = [] @@ -312,7 +350,7 @@ def list_buildoptions(coredata: cdata.CoreData): add_keys(optlist, dir_options, 'directory') add_keys(optlist, coredata.user_options, 'user') add_keys(optlist, test_options, 'test') - return ('buildoptions', optlist) + return optlist def add_keys(optlist, options, section): keys = list(options.keys()) @@ -349,7 +387,7 @@ def find_buildsystem_files_list(src_dir): def list_buildsystem_files(builddata: build.Build): src_dir = builddata.environment.get_source_dir() filelist = find_buildsystem_files_list(src_dir) - return ('buildsystem_files', filelist) + return filelist def list_deps(coredata: cdata.CoreData): result = [] @@ -358,7 +396,7 @@ def list_deps(coredata: cdata.CoreData): result += [{'name': d.name, 'compile_args': d.get_compile_args(), 'link_args': d.get_link_args()}] - return ('dependencies', result) + return result def get_test_list(testdata): result = [] @@ -382,10 +420,10 @@ def get_test_list(testdata): return result def list_tests(testdata): - return ('tests', get_test_list(testdata)) + return get_test_list(testdata) def list_benchmarks(benchdata): - return ('benchmarks', get_test_list(benchdata)) + return get_test_list(benchdata) def list_projinfo(builddata: build.Build): result = {'version': builddata.project_version, @@ -397,7 +435,7 @@ def list_projinfo(builddata: build.Build): 'descriptive_name': builddata.projects.get(k)} subprojects.append(c) result['subprojects'] = subprojects - return ('projectinfo', result) + return result class ProjectInfoInterperter(astinterpreter.AstInterpreter): def __init__(self, source_root, subdir): @@ -482,30 +520,21 @@ def run(options): results = [] toextract = [] + intro_types = get_meson_introspection_types() + + for i in intro_types.keys(): + if options.all or getattr(options, i, False): + toextract += [i] - if options.all or options.benchmarks: - toextract += ['benchmarks'] - if options.all or options.buildoptions: - toextract += ['buildoptions'] - if options.all or options.buildsystem_files: - toextract += ['buildsystem_files'] - if options.all or options.dependencies: - toextract += ['dependencies'] - if options.all or options.list_installed: - toextract += ['installed'] - if options.all or options.projectinfo: - toextract += ['projectinfo'] - if options.all or options.list_targets: - toextract += ['targets'] + # 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') with open(targets_file, 'r') as fp: targets = json.load(fp) builddata = build.load(options.builddir) # TODO remove this in a breaking changes PR - results += [list_target_files(options.target_files, targets, builddata)] - if options.all or options.tests: - toextract += ['tests'] + results += [('target_files', list_target_files(options.target_files, targets, builddata))] + # Extract introspection information from JSON for i in toextract: curr = os.path.join(infodir, 'intro-{}.json'.format(i)) if not os.path.isfile(curr): @@ -527,7 +556,10 @@ def run(options): print(json.dumps(out, indent=indent)) return 0 +updated_introspection_files = [] + def write_intro_info(intro_info, info_dir): + global updated_introspection_files for i in intro_info: out_file = os.path.join(info_dir, 'intro-{}.json'.format(i[0])) tmp_file = os.path.join(info_dir, 'tmp_dump.json') @@ -535,29 +567,70 @@ def write_intro_info(intro_info, info_dir): json.dump(i[1], fp) fp.flush() # Not sure if this is needed os.replace(tmp_file, out_file) + updated_introspection_files += [i[0]] def generate_introspection_file(builddata: build.Build, backend: backends.Backend): coredata = builddata.environment.get_coredata() - benchmarkdata = backend.create_test_serialisation(builddata.get_benchmarks()) - testdata = backend.create_test_serialisation(builddata.get_tests()) - installdata = backend.create_install_data() + intro_types = get_meson_introspection_types(coredata=coredata, builddata=builddata, backend=backend) + intro_info = [] - intro_info = [ - list_benchmarks(benchmarkdata), - list_buildoptions(coredata), - list_buildsystem_files(builddata), - list_deps(coredata), - list_installed(installdata), - list_projinfo(builddata), - list_targets(builddata, installdata, backend), - list_tests(testdata) - ] + for key, val in intro_types.items(): + intro_info += [(key, val['func']())] write_intro_info(intro_info, builddata.environment.info_dir) def update_build_options(coredata: cdata.CoreData, info_dir): intro_info = [ - list_buildoptions(coredata) + ('buildoptions', list_buildoptions(coredata)) ] write_intro_info(intro_info, info_dir) + +def split_version_string(version: str): + vers_list = version.split('.') + return { + 'full': version, + 'major': int(vers_list[0] if len(vers_list) > 0 else 0), + 'minor': int(vers_list[1] if len(vers_list) > 1 else 0), + 'patch': int(vers_list[2] if len(vers_list) > 2 else 0) + } + +def write_meson_info_file(builddata: build.Build, errors: list, build_files_updated: bool = False): + global updated_introspection_files + info_dir = builddata.environment.info_dir + info_file = get_meson_info_file(info_dir) + intro_types = get_meson_introspection_types() + intro_info = {} + + for i in intro_types.keys(): + intro_info[i] = { + 'file': 'intro-{}.json'.format(i), + 'updated': i in updated_introspection_files + } + + info_data = { + 'meson_version': split_version_string(cdata.version), + 'directories': { + 'source': builddata.environment.get_source_dir(), + 'build': builddata.environment.get_build_dir(), + 'info': info_dir, + }, + 'introspection': { + 'version': split_version_string(get_meson_introspection_version()), + 'information': intro_info, + }, + 'build_files_updated': build_files_updated, + } + + if len(errors) > 0: + info_data['error'] = True + info_data['error_list'] = [x if isinstance(x, str) else str(x) for x in errors] + else: + info_data['error'] = False + + # Write the data to disc + tmp_file = os.path.join(info_dir, 'tmp_dump.json') + with open(tmp_file, 'w') as fp: + json.dump(info_data, fp) + fp.flush() + os.replace(tmp_file, info_file) diff --git a/mesonbuild/msetup.py b/mesonbuild/msetup.py index 67559a188..023afdbb0 100644 --- a/mesonbuild/msetup.py +++ b/mesonbuild/msetup.py @@ -185,11 +185,15 @@ class MesonApp: mlog.log('Target machine cpu:', mlog.bold(intr.builtin['target_machine'].cpu_method([], {}))) mlog.log('Build machine cpu family:', mlog.bold(intr.builtin['build_machine'].cpu_family_method([], {}))) mlog.log('Build machine cpu:', mlog.bold(intr.builtin['build_machine'].cpu_method([], {}))) - if self.options.profile: - fname = os.path.join(self.build_dir, 'meson-private', 'profile-interpreter.log') - profile.runctx('intr.run()', globals(), locals(), filename=fname) - else: - intr.run() + try: + if self.options.profile: + fname = os.path.join(self.build_dir, 'meson-private', 'profile-interpreter.log') + profile.runctx('intr.run()', globals(), locals(), filename=fname) + else: + intr.run() + except Exception as e: + mintro.write_meson_info_file(b, [e]) + raise # Print all default option values that don't match the current value for def_opt_name, def_opt_value, cur_opt_value in intr.get_non_matching_default_options(): mlog.log('Option', mlog.bold(def_opt_name), 'is:', @@ -224,7 +228,9 @@ class MesonApp: profile.runctx('mintro.generate_introspection_file(b, intr.backend)', globals(), locals(), filename=fname) else: mintro.generate_introspection_file(b, intr.backend) - except: + mintro.write_meson_info_file(b, [], True) + except Exception as e: + mintro.write_meson_info_file(b, [e]) if 'cdf' in locals(): old_cdf = cdf + '.prev' if os.path.exists(old_cdf): diff --git a/run_unittests.py b/run_unittests.py index f7737ab57..55a5bd6d2 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -3291,6 +3291,20 @@ recommended as it is not supported on some platforms''') self.assertEqual(res_all, res_file) + def test_introspect_meson_info(self): + testdir = os.path.join(self.unit_test_dir, '49 introspection') + introfile = os.path.join(self.builddir, 'meson-info', 'meson-info.json') + self.init(testdir) + self.assertPathExists(introfile) + with open(introfile, 'r') as fp: + res1 = json.load(fp) + + for i in ['meson_version', 'directories', 'introspection', 'build_files_updated', 'error']: + self.assertIn(i, res1) + + self.assertEqual(res1['error'], False) + self.assertEqual(res1['build_files_updated'], True) + def test_introspect_config_update(self): testdir = os.path.join(self.unit_test_dir, '49 introspection') introfile = os.path.join(self.builddir, 'meson-info', 'intro-buildoptions.json')