1# Copyright 2014-2016 The Meson development team
2
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6
7#     http://www.apache.org/licenses/LICENSE-2.0
8
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""This is a helper script for IDE developers. It allows you to
16extract information such as list of targets, files, compiler flags,
17tests and so on. All output is in JSON for simple parsing.
18
19Currently only works for the Ninja backend. Others use generated
20project files and don't need this info."""
21
22import collections
23import json
24from . import build, coredata as cdata
25from . import mesonlib
26from .ast import IntrospectionInterpreter, build_target_functions, AstConditionLevel, AstIDGenerator, AstIndentationGenerator, AstJSONPrinter
27from . import mlog
28from .backend import backends
29from .mparser import BaseNode, FunctionNode, ArrayNode, ArgumentNode, StringNode
30from .interpreter import Interpreter
31from pathlib import Path, PurePath
32import typing as T
33import os
34import argparse
35
36from .mesonlib import OptionKey
37
38def get_meson_info_file(info_dir: str) -> str:
39    return os.path.join(info_dir, 'meson-info.json')
40
41def get_meson_introspection_version() -> str:
42    return '1.0.0'
43
44def get_meson_introspection_required_version() -> T.List[str]:
45    return ['>=1.0', '<2.0']
46
47class IntroCommand:
48    def __init__(self,
49                 desc: str,
50                 func: T.Optional[T.Callable[[], T.Union[dict, list]]] = None,
51                 no_bd: T.Optional[T.Callable[[IntrospectionInterpreter], T.Union[dict, list]]] = None) -> None:
52        self.desc = desc + '.'
53        self.func = func
54        self.no_bd = no_bd
55
56def get_meson_introspection_types(coredata: T.Optional[cdata.CoreData] = None,
57                                  builddata: T.Optional[build.Build] = None,
58                                  backend: T.Optional[backends.Backend] = None,
59                                  sourcedir: T.Optional[str] = None) -> 'T.Mapping[str, IntroCommand]':
60    if backend and builddata:
61        benchmarkdata = backend.create_test_serialisation(builddata.get_benchmarks())
62        testdata = backend.create_test_serialisation(builddata.get_tests())
63        installdata = backend.create_install_data()
64        interpreter = backend.interpreter
65    else:
66        benchmarkdata = testdata = installdata = None
67
68    # Enforce key order for argparse
69    return collections.OrderedDict([
70        ('ast', IntroCommand('Dump the AST of the meson file', no_bd=dump_ast)),
71        ('benchmarks', IntroCommand('List all benchmarks', func=lambda: list_benchmarks(benchmarkdata))),
72        ('buildoptions', IntroCommand('List all build options', func=lambda: list_buildoptions(coredata), no_bd=list_buildoptions_from_source)),
73        ('buildsystem_files', IntroCommand('List files that make up the build system', func=lambda: list_buildsystem_files(builddata, interpreter))),
74        ('dependencies', IntroCommand('List external dependencies', func=lambda: list_deps(coredata), no_bd=list_deps_from_source)),
75        ('scan_dependencies', IntroCommand('Scan for dependencies used in the meson.build file', no_bd=list_deps_from_source)),
76        ('installed', IntroCommand('List all installed files and directories', func=lambda: list_installed(installdata))),
77        ('install_plan', IntroCommand('List all installed files and directories with their details', func=lambda: list_install_plan(installdata))),
78        ('projectinfo', IntroCommand('Information about projects', func=lambda: list_projinfo(builddata), no_bd=list_projinfo_from_source)),
79        ('targets', IntroCommand('List top level targets', func=lambda: list_targets(builddata, installdata, backend), no_bd=list_targets_from_source)),
80        ('tests', IntroCommand('List all unit tests', func=lambda: list_tests(testdata))),
81    ])
82
83def add_arguments(parser: argparse.ArgumentParser) -> None:
84    intro_types = get_meson_introspection_types()
85    for key, val in intro_types.items():
86        flag = '--' + key.replace('_', '-')
87        parser.add_argument(flag, action='store_true', dest=key, default=False, help=val.desc)
88
89    parser.add_argument('--backend', choices=sorted(cdata.backendlist), dest='backend', default='ninja',
90                        help='The backend to use for the --buildoptions introspection.')
91    parser.add_argument('-a', '--all', action='store_true', dest='all', default=False,
92                        help='Print all available information.')
93    parser.add_argument('-i', '--indent', action='store_true', dest='indent', default=False,
94                        help='Enable pretty printed JSON.')
95    parser.add_argument('-f', '--force-object-output', action='store_true', dest='force_dict', default=False,
96                        help='Always use the new JSON format for multiple entries (even for 0 and 1 introspection commands)')
97    parser.add_argument('builddir', nargs='?', default='.', help='The build directory')
98
99def dump_ast(intr: IntrospectionInterpreter) -> T.Dict[str, T.Any]:
100    printer = AstJSONPrinter()
101    intr.ast.accept(printer)
102    return printer.result
103
104def list_installed(installdata: backends.InstallData) -> T.Dict[str, str]:
105    res = {}
106    if installdata is not None:
107        for t in installdata.targets:
108            res[os.path.join(installdata.build_dir, t.fname)] = \
109                os.path.join(installdata.prefix, t.outdir, os.path.basename(t.fname))
110            for alias in t.aliases.keys():
111                res[os.path.join(installdata.build_dir, alias)] = \
112                    os.path.join(installdata.prefix, t.outdir, os.path.basename(alias))
113        for i in installdata.data:
114            res[i.path] = os.path.join(installdata.prefix, i.install_path)
115        for i in installdata.headers:
116            res[i.path] = os.path.join(installdata.prefix, i.install_path, os.path.basename(i.path))
117        for i in installdata.man:
118            res[i.path] = os.path.join(installdata.prefix, i.install_path)
119        for i in installdata.install_subdirs:
120            res[i.path] = os.path.join(installdata.prefix, i.install_path)
121    return res
122
123def list_install_plan(installdata: backends.InstallData) -> T.Dict[str, T.Dict[str, T.Dict[str, T.Optional[str]]]]:
124    plan = {
125        'targets': {
126            os.path.join(installdata.build_dir, target.fname): {
127                'destination': target.out_name,
128                'tag': target.tag or None,
129            }
130            for target in installdata.targets
131        },
132    }  # type: T.Dict[str, T.Dict[str, T.Dict[str, T.Optional[str]]]]
133    for key, data_list in {
134        'data': installdata.data,
135        'man': installdata.man,
136        'headers': installdata.headers,
137    }.items():
138        for data in data_list:
139            data_type = data.data_type or key
140            install_path_name = data.install_path_name
141            if key == 'headers':  # in the headers, install_path_name is the directory
142                install_path_name = os.path.join(install_path_name, os.path.basename(data.path))
143            elif data_type == 'configure':
144                install_path_name = os.path.join('{prefix}', install_path_name)
145
146            plan[data_type] = plan.get(data_type, {})
147            plan[data_type][data.path] = {
148                'destination': install_path_name,
149                'tag': data.tag or None,
150            }
151    return plan
152
153def get_target_dir(coredata: cdata.CoreData, subdir: str) -> str:
154    if coredata.get_option(OptionKey('layout')) == 'flat':
155        return 'meson-out'
156    else:
157        return subdir
158
159def list_targets_from_source(intr: IntrospectionInterpreter) -> T.List[T.Dict[str, T.Union[bool, str, T.List[T.Union[str, T.Dict[str, T.Union[str, T.List[str], bool]]]]]]]:
160    tlist = []  # type: T.List[T.Dict[str, T.Union[bool, str, T.List[T.Union[str, T.Dict[str, T.Union[str, T.List[str], bool]]]]]]]
161    root_dir = Path(intr.source_root)
162
163    def nodes_to_paths(node_list: T.List[BaseNode]) -> T.List[Path]:
164        res = []  # type: T.List[Path]
165        for n in node_list:
166            args = []  # type: T.List[BaseNode]
167            if isinstance(n, FunctionNode):
168                args = list(n.args.arguments)
169                if n.func_name in build_target_functions:
170                    args.pop(0)
171            elif isinstance(n, ArrayNode):
172                args = n.args.arguments
173            elif isinstance(n, ArgumentNode):
174                args = n.arguments
175            for j in args:
176                if isinstance(j, StringNode):
177                    assert isinstance(j.value, str)
178                    res += [Path(j.value)]
179                elif isinstance(j, str):
180                    res += [Path(j)]
181        res = [root_dir / i['subdir'] / x for x in res]
182        res = [x.resolve() for x in res]
183        return res
184
185    for i in intr.targets:
186        sources = nodes_to_paths(i['sources'])
187        extra_f = nodes_to_paths(i['extra_files'])
188        outdir = get_target_dir(intr.coredata, i['subdir'])
189
190        tlist += [{
191            'name': i['name'],
192            'id': i['id'],
193            'type': i['type'],
194            'defined_in': i['defined_in'],
195            'filename': [os.path.join(outdir, x) for x in i['outputs']],
196            'build_by_default': i['build_by_default'],
197            'target_sources': [{
198                'language': 'unknown',
199                'compiler': [],
200                'parameters': [],
201                'sources': [str(x) for x in sources],
202                'generated_sources': []
203            }],
204            'extra_files': [str(x) for x in extra_f],
205            'subproject': None, # Subprojects are not supported
206            'installed': i['installed']
207        }]
208
209    return tlist
210
211def list_targets(builddata: build.Build, installdata: backends.InstallData, backend: backends.Backend) -> T.List[T.Any]:
212    tlist = []  # type: T.List[T.Any]
213    build_dir = builddata.environment.get_build_dir()
214    src_dir = builddata.environment.get_source_dir()
215
216    # Fast lookup table for installation files
217    install_lookuptable = {}
218    for i in installdata.targets:
219        out = [os.path.join(installdata.prefix, i.outdir, os.path.basename(i.fname))]
220        out += [os.path.join(installdata.prefix, i.outdir, os.path.basename(x)) for x in i.aliases]
221        install_lookuptable[os.path.basename(i.fname)] = [str(PurePath(x)) for x in out]
222
223    for (idname, target) in builddata.get_targets().items():
224        if not isinstance(target, build.Target):
225            raise RuntimeError('The target object in `builddata.get_targets()` is not of type `build.Target`. Please file a bug with this error message.')
226
227        outdir = get_target_dir(builddata.environment.coredata, target.subdir)
228        t = {
229            'name': target.get_basename(),
230            'id': idname,
231            'type': target.get_typename(),
232            'defined_in': os.path.normpath(os.path.join(src_dir, target.subdir, 'meson.build')),
233            'filename': [os.path.join(build_dir, outdir, x) for x in target.get_outputs()],
234            'build_by_default': target.build_by_default,
235            'target_sources': backend.get_introspection_data(idname, target),
236            'extra_files': [os.path.normpath(os.path.join(src_dir, x.subdir, x.fname)) for x in target.extra_files],
237            'subproject': target.subproject or None
238        }
239
240        if installdata and target.should_install():
241            t['installed'] = True
242            ifn = [install_lookuptable.get(x, [None]) for x in target.get_outputs()]
243            t['install_filename'] = [x for sublist in ifn for x in sublist]  # flatten the list
244        else:
245            t['installed'] = False
246        tlist.append(t)
247    return tlist
248
249def list_buildoptions_from_source(intr: IntrospectionInterpreter) -> T.List[T.Dict[str, T.Union[str, bool, int, T.List[str]]]]:
250    subprojects = [i['name'] for i in intr.project_data['subprojects']]
251    return list_buildoptions(intr.coredata, subprojects)
252
253def list_buildoptions(coredata: cdata.CoreData, subprojects: T.Optional[T.List[str]] = None) -> T.List[T.Dict[str, T.Union[str, bool, int, T.List[str]]]]:
254    optlist = []  # type: T.List[T.Dict[str, T.Union[str, bool, int, T.List[str]]]]
255    subprojects = subprojects or []
256
257    dir_option_names = set(cdata.BUILTIN_DIR_OPTIONS)
258    test_option_names = {OptionKey('errorlogs'),
259                         OptionKey('stdsplit')}
260
261    dir_options: 'cdata.KeyedOptionDictType' = {}
262    test_options: 'cdata.KeyedOptionDictType' = {}
263    core_options: 'cdata.KeyedOptionDictType' = {}
264    for k, v in coredata.options.items():
265        if k in dir_option_names:
266            dir_options[k] = v
267        elif k in test_option_names:
268            test_options[k] = v
269        elif k.is_builtin():
270            core_options[k] = v
271            if not v.yielding:
272                for s in subprojects:
273                    core_options[k.evolve(subproject=s)] = v
274
275    def add_keys(options: 'cdata.KeyedOptionDictType', section: str) -> None:
276        for key, opt in sorted(options.items()):
277            optdict = {'name': str(key), 'value': opt.value, 'section': section,
278                       'machine': key.machine.get_lower_case_name() if coredata.is_per_machine_option(key) else 'any'}
279            if isinstance(opt, cdata.UserStringOption):
280                typestr = 'string'
281            elif isinstance(opt, cdata.UserBooleanOption):
282                typestr = 'boolean'
283            elif isinstance(opt, cdata.UserComboOption):
284                optdict['choices'] = opt.choices
285                typestr = 'combo'
286            elif isinstance(opt, cdata.UserIntegerOption):
287                typestr = 'integer'
288            elif isinstance(opt, cdata.UserArrayOption):
289                typestr = 'array'
290                if opt.choices:
291                    optdict['choices'] = opt.choices
292            else:
293                raise RuntimeError("Unknown option type")
294            optdict['type'] = typestr
295            optdict['description'] = opt.description
296            optlist.append(optdict)
297
298    add_keys(core_options, 'core')
299    add_keys({k: v for k, v in coredata.options.items() if k.is_backend()}, 'backend')
300    add_keys({k: v for k, v in coredata.options.items() if k.is_base()}, 'base')
301    add_keys(
302        {k: v for k, v in sorted(coredata.options.items(), key=lambda i: i[0].machine) if k.is_compiler()},
303        'compiler',
304    )
305    add_keys(dir_options, 'directory')
306    add_keys({k: v for k, v in coredata.options.items() if k.is_project()}, 'user')
307    add_keys(test_options, 'test')
308    return optlist
309
310def find_buildsystem_files_list(src_dir: str) -> T.List[str]:
311    # I feel dirty about this. But only slightly.
312    filelist = []  # type: T.List[str]
313    for root, _, files in os.walk(src_dir):
314        for f in files:
315            if f == 'meson.build' or f == 'meson_options.txt':
316                filelist.append(os.path.relpath(os.path.join(root, f), src_dir))
317    return filelist
318
319def list_buildsystem_files(builddata: build.Build, interpreter: Interpreter) -> T.List[str]:
320    src_dir = builddata.environment.get_source_dir()
321    filelist = interpreter.get_build_def_files()  # type: T.List[str]
322    filelist = [PurePath(src_dir, x).as_posix() for x in filelist]
323    return filelist
324
325def list_deps_from_source(intr: IntrospectionInterpreter) -> T.List[T.Dict[str, T.Union[str, bool]]]:
326    result = []  # type: T.List[T.Dict[str, T.Union[str, bool]]]
327    for i in intr.dependencies:
328        keys = [
329            'name',
330            'required',
331            'version',
332            'has_fallback',
333            'conditional',
334        ]
335        result += [{k: v for k, v in i.items() if k in keys}]
336    return result
337
338def list_deps(coredata: cdata.CoreData) -> T.List[T.Dict[str, T.Union[str, T.List[str]]]]:
339    result = []  # type: T.List[T.Dict[str, T.Union[str, T.List[str]]]]
340    for d in coredata.deps.host.values():
341        if d.found():
342            result += [{'name': d.name,
343                        'version': d.get_version(),
344                        'compile_args': d.get_compile_args(),
345                        'link_args': d.get_link_args()}]
346    return result
347
348def get_test_list(testdata: T.List[backends.TestSerialisation]) -> T.List[T.Dict[str, T.Union[str, int, T.List[str], T.Dict[str, str]]]]:
349    result = []  # type: T.List[T.Dict[str, T.Union[str, int, T.List[str], T.Dict[str, str]]]]
350    for t in testdata:
351        to = {}  # type: T.Dict[str, T.Union[str, int, T.List[str], T.Dict[str, str]]]
352        if isinstance(t.fname, str):
353            fname = [t.fname]
354        else:
355            fname = t.fname
356        to['cmd'] = fname + t.cmd_args
357        if isinstance(t.env, build.EnvironmentVariables):
358            to['env'] = t.env.get_env({})
359        else:
360            to['env'] = t.env
361        to['name'] = t.name
362        to['workdir'] = t.workdir
363        to['timeout'] = t.timeout
364        to['suite'] = t.suite
365        to['is_parallel'] = t.is_parallel
366        to['priority'] = t.priority
367        to['protocol'] = str(t.protocol)
368        to['depends'] = t.depends
369        result.append(to)
370    return result
371
372def list_tests(testdata: T.List[backends.TestSerialisation]) -> T.List[T.Dict[str, T.Union[str, int, T.List[str], T.Dict[str, str]]]]:
373    return get_test_list(testdata)
374
375def list_benchmarks(benchdata: T.List[backends.TestSerialisation]) -> T.List[T.Dict[str, T.Union[str, int, T.List[str], T.Dict[str, str]]]]:
376    return get_test_list(benchdata)
377
378def list_projinfo(builddata: build.Build) -> T.Dict[str, T.Union[str, T.List[T.Dict[str, str]]]]:
379    result = {'version': builddata.project_version,
380              'descriptive_name': builddata.project_name,
381              'subproject_dir': builddata.subproject_dir}    # type: T.Dict[str, T.Union[str, T.List[T.Dict[str, str]]]]
382    subprojects = []
383    for k, v in builddata.subprojects.items():
384        c = {'name': k,
385             'version': v,
386             'descriptive_name': builddata.projects.get(k)}  # type: T.Dict[str, str]
387        subprojects.append(c)
388    result['subprojects'] = subprojects
389    return result
390
391def list_projinfo_from_source(intr: IntrospectionInterpreter) -> T.Dict[str, T.Union[str, T.List[T.Dict[str, str]]]]:
392    sourcedir = intr.source_root
393    files = find_buildsystem_files_list(sourcedir)
394    files = [os.path.normpath(x) for x in files]
395
396    for i in intr.project_data['subprojects']:
397        basedir = os.path.join(intr.subproject_dir, i['name'])
398        i['buildsystem_files'] = [x for x in files if x.startswith(basedir)]
399        files = [x for x in files if not x.startswith(basedir)]
400
401    intr.project_data['buildsystem_files'] = files
402    intr.project_data['subproject_dir'] = intr.subproject_dir
403    return intr.project_data
404
405def print_results(options: argparse.Namespace, results: T.Sequence[T.Tuple[str, T.Union[dict, T.List[T.Any]]]], indent: int) -> int:
406    if not results and not options.force_dict:
407        print('No command specified')
408        return 1
409    elif len(results) == 1 and not options.force_dict:
410        # Make to keep the existing output format for a single option
411        print(json.dumps(results[0][1], indent=indent))
412    else:
413        out = {}
414        for i in results:
415            out[i[0]] = i[1]
416        print(json.dumps(out, indent=indent))
417    return 0
418
419def get_infodir(builddir: T.Optional[str] = None) -> str:
420    infodir = 'meson-info'
421    if builddir is not None:
422        infodir = os.path.join(builddir, infodir)
423    return infodir
424
425def get_info_file(infodir: str, kind: T.Optional[str] = None) -> str:
426    return os.path.join(infodir,
427                        'meson-info.json' if not kind else f'intro-{kind}.json')
428
429def load_info_file(infodir: str, kind: T.Optional[str] = None) -> T.Any:
430    with open(get_info_file(infodir, kind), encoding='utf-8') as fp:
431        return json.load(fp)
432
433def run(options: argparse.Namespace) -> int:
434    datadir = 'meson-private'
435    infodir = get_infodir(options.builddir)
436    if options.builddir is not None:
437        datadir = os.path.join(options.builddir, datadir)
438    indent = 4 if options.indent else None
439    results = []  # type: T.List[T.Tuple[str, T.Union[dict, T.List[T.Any]]]]
440    sourcedir = '.' if options.builddir == 'meson.build' else options.builddir[:-11]
441    intro_types = get_meson_introspection_types(sourcedir=sourcedir)
442
443    if 'meson.build' in [os.path.basename(options.builddir), options.builddir]:
444        # Make sure that log entries in other parts of meson don't interfere with the JSON output
445        mlog.disable()
446        backend = backends.get_backend_from_name(options.backend)
447        assert backend is not None
448        intr = IntrospectionInterpreter(sourcedir, '', backend.name, visitors = [AstIDGenerator(), AstIndentationGenerator(), AstConditionLevel()])
449        intr.analyze()
450        # Re-enable logging just in case
451        mlog.enable()
452        for key, val in intro_types.items():
453            if (not options.all and not getattr(options, key, False)) or not val.no_bd:
454                continue
455            results += [(key, val.no_bd(intr))]
456        return print_results(options, results, indent)
457
458    try:
459        raw = load_info_file(infodir)
460        intro_vers = raw.get('introspection', {}).get('version', {}).get('full', '0.0.0')
461    except FileNotFoundError:
462        if not os.path.isdir(datadir) or not os.path.isdir(infodir):
463            print('Current directory is not a meson build directory.\n'
464                  'Please specify a valid build dir or change the working directory to it.')
465        else:
466            print('Introspection file {} does not exist.\n'
467                  'It is also possible that the build directory was generated with an old\n'
468                  'meson version. Please regenerate it in this case.'.format(get_info_file(infodir)))
469        return 1
470
471    vers_to_check = get_meson_introspection_required_version()
472    for i in vers_to_check:
473        if not mesonlib.version_compare(intro_vers, i):
474            print('Introspection version {} is not supported. '
475                  'The required version is: {}'
476                  .format(intro_vers, ' and '.join(vers_to_check)))
477            return 1
478
479    # Extract introspection information from JSON
480    for i in intro_types.keys():
481        if not intro_types[i].func:
482            continue
483        if not options.all and not getattr(options, i, False):
484            continue
485        try:
486            results += [(i, load_info_file(infodir, i))]
487        except FileNotFoundError:
488            print('Introspection file {} does not exist.'.format(get_info_file(infodir, i)))
489            return 1
490
491    return print_results(options, results, indent)
492
493updated_introspection_files = []  # type: T.List[str]
494
495def write_intro_info(intro_info: T.Sequence[T.Tuple[str, T.Union[dict, T.List[T.Any]]]], info_dir: str) -> None:
496    global updated_introspection_files
497    for kind, data in intro_info:
498        out_file = os.path.join(info_dir, f'intro-{kind}.json')
499        tmp_file = os.path.join(info_dir, 'tmp_dump.json')
500        with open(tmp_file, 'w', encoding='utf-8') as fp:
501            json.dump(data, fp)
502            fp.flush() # Not sure if this is needed
503        os.replace(tmp_file, out_file)
504        updated_introspection_files += [kind]
505
506def generate_introspection_file(builddata: build.Build, backend: backends.Backend) -> None:
507    coredata = builddata.environment.get_coredata()
508    intro_types = get_meson_introspection_types(coredata=coredata, builddata=builddata, backend=backend)
509    intro_info = []  # type: T.List[T.Tuple[str, T.Union[dict, T.List[T.Any]]]]
510
511    for key, val in intro_types.items():
512        if not val.func:
513            continue
514        intro_info += [(key, val.func())]
515
516    write_intro_info(intro_info, builddata.environment.info_dir)
517
518def update_build_options(coredata: cdata.CoreData, info_dir: str) -> None:
519    intro_info = [
520        ('buildoptions', list_buildoptions(coredata))
521    ]
522
523    write_intro_info(intro_info, info_dir)
524
525def split_version_string(version: str) -> T.Dict[str, T.Union[str, int]]:
526    vers_list = version.split('.')
527    return {
528        'full': version,
529        'major': int(vers_list[0] if len(vers_list) > 0 else 0),
530        'minor': int(vers_list[1] if len(vers_list) > 1 else 0),
531        'patch': int(vers_list[2] if len(vers_list) > 2 else 0)
532    }
533
534def write_meson_info_file(builddata: build.Build, errors: list, build_files_updated: bool = False) -> None:
535    global updated_introspection_files
536    info_dir = builddata.environment.info_dir
537    info_file = get_meson_info_file(info_dir)
538    intro_types = get_meson_introspection_types()
539    intro_info = {}
540
541    for i in intro_types.keys():
542        if not intro_types[i].func:
543            continue
544        intro_info[i] = {
545            'file': f'intro-{i}.json',
546            'updated': i in updated_introspection_files
547        }
548
549    info_data = {
550        'meson_version': split_version_string(cdata.version),
551        'directories': {
552            'source': builddata.environment.get_source_dir(),
553            'build': builddata.environment.get_build_dir(),
554            'info': info_dir,
555        },
556        'introspection': {
557            'version': split_version_string(get_meson_introspection_version()),
558            'information': intro_info,
559        },
560        'build_files_updated': build_files_updated,
561    }
562
563    if errors:
564        info_data['error'] = True
565        info_data['error_list'] = [x if isinstance(x, str) else str(x) for x in errors]
566    else:
567        info_data['error'] = False
568
569    # Write the data to disc
570    tmp_file = os.path.join(info_dir, 'tmp_dump.json')
571    with open(tmp_file, 'w', encoding='utf-8') as fp:
572        json.dump(info_data, fp)
573        fp.flush()
574    os.replace(tmp_file, info_file)
575