diff --git a/data/syntax-highlighting/vim/syntax/meson.vim b/data/syntax-highlighting/vim/syntax/meson.vim index 2b68db5f374d..52d3e10b74d6 100644 --- a/data/syntax-highlighting/vim/syntax/meson.vim +++ b/data/syntax-highlighting/vim/syntax/meson.vim @@ -117,6 +117,7 @@ syn keyword mesonBuiltin \ subdir \ subdir_done \ subproject + \ summary \ target_machine \ test \ vcs_tag diff --git a/docs/markdown/Reference-manual.md b/docs/markdown/Reference-manual.md index 5c5d164e9a34..f84deb7c5613 100644 --- a/docs/markdown/Reference-manual.md +++ b/docs/markdown/Reference-manual.md @@ -1203,6 +1203,62 @@ This function prints its argument to stdout prefixed with WARNING:. *Added 0.44.0* +### summary() + +``` meson + void summary(section, value) +``` + +This function is used to summarize build configuration at the end of the build +process. This function provides a way for projects (and subprojects) to report +this information in a clear way. + +The first argument is a section name, the second argument is either a string +or a dictionary. `summary()` can be called multiple times with a different +section name at each invocation. All sections will be collected and printed at +the end of the configuration in the same order as they have been called. + +- `summary('section1', 'foo')` will print `Section1 = foo`, +- `summary('section2', {'foo': 'bar'})` will print: + ``` + Section1 + foo = bar + ``` + +Dictionaries values can only be lists, dictionaries, integers, booleans or strings. +Lists and dictionaries values will be pretty printed as `['a', 'b', ...]` and +`{'a': b, ...}` respectively. + +Example: +```meson +sec1 = {'driver' : 'foobar', 'OS' : 'Linux', 'API' : '1.7'} +sec2 = {'driver' : 'dive comp', 'OS' : 'Minix', 'API' : '1.1.2'} +sec3 = {'with' : {'mesa' : true, 'gbm' : false}} + +summary('Backend', 'OpenGL') +summary('Server', sec1) +summary('Client', sec2) +summary('Misc', sec3) +``` + +Output: +``` +Main Project: + Backend = OpenGL + Server + driver = 'foobar' + OS = 'Linux' + API = '1.7' + Client + driver = 'dive comp' + OS = 'Minix' + API = '1.1.2' + Misc + with = {'mesa' : True, 'gbm' : False} +``` + +*Added 0.53.0* + ### project() ``` meson diff --git a/docs/markdown/snippets/summary.md b/docs/markdown/snippets/summary.md new file mode 100644 index 000000000000..256814eb1b4d --- /dev/null +++ b/docs/markdown/snippets/summary.md @@ -0,0 +1,32 @@ +## Add a new summary() function + +A new function [`summary()`](Reference-manual.md#summary) has been added to +summarize build configuration at the end of the build process. + +Example: +```meson +sec1 = {'driver' : 'foobar', 'OS' : 'Linux', 'API' : '1.7'} +sec2 = {'driver' : 'dive comp', 'OS' : 'Minix', 'API' : '1.1.2'} +sec3 = {'with' : {'mesa' : true, 'gbm' : false}} + +summary('Backend', 'OpenGL') +summary('Server', sec1) +summary('Client', sec2) +summary('Misc', sec3) +``` + +Output: +``` +Main Project: + Backend = OpenGL + Server + driver = 'foobar' + OS = 'Linux' + API = '1.7' + Client + driver = 'dive comp' + OS = 'Minix' + API = '1.1.2' + Misc + with = {'mesa' : True, 'gbm' : False} +``` diff --git a/mesonbuild/ast/interpreter.py b/mesonbuild/ast/interpreter.py index 847f81783458..ebdde3fff2b8 100644 --- a/mesonbuild/ast/interpreter.py +++ b/mesonbuild/ast/interpreter.py @@ -119,6 +119,7 @@ def __init__(self, source_root: str, subdir: str, visitors: Optional[List[AstVis 'find_library': self.func_do_nothing, 'subdir_done': self.func_do_nothing, 'alias_target': self.func_do_nothing, + 'summary': self.func_do_nothing, }) def func_do_nothing(self, node, args, kwargs): diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index 024266eea8c1..ade0cbe66d0f 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -38,7 +38,7 @@ import os, shutil, uuid import re, shlex import subprocess -from collections import namedtuple +import collections from itertools import chain import functools from typing import Sequence, List, Union, Optional, Dict, Any @@ -50,7 +50,7 @@ 'sources'}, } -def stringifyUserArguments(args): +def stringifyUserArguments(args, quote_string=True): if isinstance(args, list): return '[%s]' % ', '.join([stringifyUserArguments(x) for x in args]) elif isinstance(args, dict): @@ -58,7 +58,7 @@ def stringifyUserArguments(args): elif isinstance(args, int): return str(args) elif isinstance(args, str): - return "'%s'" % args + return '{!r}'.format(args) if quote_string else args raise InvalidArguments('Function accepts only strings, integers, lists and lists thereof.') @@ -1691,7 +1691,7 @@ def get_argument_syntax_method(self, args, kwargs): return self.compiler.get_argument_syntax() -ModuleState = namedtuple('ModuleState', [ +ModuleState = collections.namedtuple('ModuleState', [ 'source_root', 'build_to_src', 'subproject', 'subdir', 'current_lineno', 'environment', 'project_name', 'project_version', 'backend', 'targets', 'data', 'headers', 'man', 'global_args', 'project_args', 'build_machine', @@ -2078,6 +2078,7 @@ def __init__(self, build, backend=None, subproject='', subdir='', subproject_dir self.coredata = self.environment.get_coredata() self.backend = backend self.subproject = subproject + self.summary = collections.defaultdict(collections.OrderedDict) if modules is None: self.modules = {} else: @@ -2188,6 +2189,7 @@ def build_func_dict(self): 'subdir': self.func_subdir, 'subdir_done': self.func_subdir_done, 'subproject': self.func_subproject, + 'summary': self.func_summary, 'shared_library': self.func_shared_lib, 'shared_module': self.func_shared_module, 'static_library': self.func_static_lib, @@ -2584,6 +2586,7 @@ def _do_subproject_meson(self, dirname, subdir, default_options, kwargs, ast=Non self.build_def_files = list(set(self.build_def_files + subi.build_def_files)) self.build.merge(subi.build) self.build.subprojects[dirname] = subi.project_version + self.summary.update(subi.summary) return self.subprojects[dirname] def _do_subproject_cmake(self, dirname, subdir, subdir_abs, default_options, kwargs): @@ -2792,38 +2795,68 @@ def func_add_languages(self, node, args, kwargs): return False return self.add_languages(args, required) - def get_message_string_arg(self, node): - # reduce arguments again to avoid flattening posargs - (posargs, _) = self.reduce_arguments(node.args) + def get_message_string_arg(self, posargs): if len(posargs) != 1: raise InvalidArguments('Expected 1 argument, got %d' % len(posargs)) + return stringifyUserArguments(posargs[0], quote_string=False) - arg = posargs[0] - if isinstance(arg, list): - argstr = stringifyUserArguments(arg) - elif isinstance(arg, dict): - argstr = stringifyUserArguments(arg) - elif isinstance(arg, str): - argstr = arg - elif isinstance(arg, int): - argstr = str(arg) - else: - raise InvalidArguments('Function accepts only strings, integers, lists and lists thereof.') - - return argstr - + @noArgsFlattening @noKwargs def func_message(self, node, args, kwargs): - argstr = self.get_message_string_arg(node) + argstr = self.get_message_string_arg(args) self.message_impl(argstr) def message_impl(self, argstr): mlog.log(mlog.bold('Message:'), argstr) + @noArgsFlattening + @noKwargs + @FeatureNew('summary', '0.53.0') + def func_summary(self, node, args, kwargs): + if len(args) != 2: + raise InterpreterException('Summary accepts exactly two arguments.') + name, value = args + if not isinstance(name, str): + raise InterpreterException('Argument 1 must be a string.') + if name in self.summary[self.subproject]: + raise InterpreterException('Options for section {!r} already set.'.format(name)) + if isinstance(value, dict): + value = {k: stringifyUserArguments(v) for k, v in value.items()} + elif not isinstance(value, str): + raise InterpreterException('Argument 2 must be string or dictionary') + self.summary[self.subproject][name] = value + + def _print_summary(self): + """Print the configuration summary. + + Called after all projects and subprojects are configured and used to + print all of the configuration at once. + """ + def printer(sections): + for name, value in sections.items(): + if isinstance(value, str): + mlog.log(' ', name, '=', value) + elif isinstance(value, dict): + mlog.log(' ', name) + for k, v in value.items(): + mlog.log(' ', k, '=', v) + mlog.log('') # newline + + mlog.log('') # newline + for project, sections in sorted(self.summary.items()): + if project == '': + continue + mlog.log(mlog.bold('Subproject {}:'.format(project))) + printer(sections) + + if '' in self.summary: + mlog.log(mlog.bold('Main Project:')) + printer(self.summary['']) + @FeatureNew('warning', '0.44.0') @noKwargs def func_warning(self, node, args, kwargs): - argstr = self.get_message_string_arg(node) + argstr = self.get_message_string_arg(args) mlog.warning(argstr, location=node) @noKwargs @@ -4060,6 +4093,8 @@ def run(self): FeatureDeprecated.report(self.subproject) if not self.is_subproject(): self.print_extra_warnings() + if self.subproject == '': + self._print_summary() def print_extra_warnings(self): # TODO cross compilation diff --git a/run_unittests.py b/run_unittests.py index ec270d7b99fa..e5d918ef1c37 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -4128,6 +4128,28 @@ def test_configure(self): self.init(testdir) self._run(self.mconf_command + [self.builddir]) + def test_summary(self): + testdir = os.path.join(self.unit_test_dir, '74 summary') + out = self.init(testdir) + self.assertRegex(out, textwrap.dedent(r''' + Subproject sub: + Features + compiled = \['foo', 'bar'\] + + Main Project: + Backend = OpenGL + Server + driver = 'foobar' + Client + (driver = 'dive comp' + API = '1.1.2')| + (API = '1.1.2' + driver = 'dive comp') + Misc + with = {'mesa|gbm' : True|False, 'gbm|mesa' : False|True} + ''')) + + class FailureTests(BasePlatformTests): ''' Tests that test failure conditions. Build files here should be dynamically diff --git a/test cases/unit/74 summary/meson.build b/test cases/unit/74 summary/meson.build new file mode 100644 index 000000000000..8515e19721c3 --- /dev/null +++ b/test cases/unit/74 summary/meson.build @@ -0,0 +1,13 @@ +project('test summary') + +sec1 = {'driver' : 'foobar'} +sec2 = {'driver' : 'dive comp', 'API' : '1.1.2'} +sec3 = {'with' : {'mesa' : true, 'gbm' : false}} + +summary('Backend', 'OpenGL') +summary('Server', sec1) +summary('Client', sec2) +summary('Misc', sec3) + +subproject('sub') +subproject('sub2', required : false) diff --git a/test cases/unit/74 summary/subprojects/sub/meson.build b/test cases/unit/74 summary/subprojects/sub/meson.build new file mode 100644 index 000000000000..efba9c12a1af --- /dev/null +++ b/test cases/unit/74 summary/subprojects/sub/meson.build @@ -0,0 +1,3 @@ +project('test summary subproject') + +summary('Features', {'compiled': ['foo', 'bar']}) diff --git a/test cases/unit/74 summary/subprojects/sub2/meson.build b/test cases/unit/74 summary/subprojects/sub2/meson.build new file mode 100644 index 000000000000..86b9cfd850f8 --- /dev/null +++ b/test cases/unit/74 summary/subprojects/sub2/meson.build @@ -0,0 +1,5 @@ +project('sub2') + +error('This subproject failed') + +summary('Section', 'Should not be seen')