Skip to content

Commit

Permalink
Add a summary() function for configuration summarization
Browse files Browse the repository at this point in the history
Based on patch from Dylan Baker.

Fixes mesonbuild#757
  • Loading branch information
dcbaker authored and xclaesse committed Dec 10, 2019
1 parent 1298f71 commit 0ccf4bb
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 23 deletions.
56 changes: 56 additions & 0 deletions docs/markdown/Reference-manual.md
Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions 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}
```
1 change: 1 addition & 0 deletions mesonbuild/ast/interpreter.py
Expand Up @@ -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):
Expand Down
81 changes: 58 additions & 23 deletions mesonbuild/interpreter.py
Expand Up @@ -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
Expand All @@ -50,15 +50,15 @@
'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):
return '{%s}' % ', '.join(['%s : %s' % (stringifyUserArguments(k), stringifyUserArguments(v)) for k, v in args.items()])
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.')


Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions run_unittests.py
Expand Up @@ -4128,6 +4128,29 @@ 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'
OS = 'Linux'
API = '1.7'
Client
driver = 'dive comp'
OS = 'Minix'
API = '1.1.2'
Misc
with = {'mesa' : True, 'gbm' : False}
'''))


class FailureTests(BasePlatformTests):
'''
Tests that test failure conditions. Build files here should be dynamically
Expand Down
13 changes: 13 additions & 0 deletions test cases/unit/74 summary/meson.build
@@ -0,0 +1,13 @@
project('test summary')

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)

subproject('sub')
subproject('sub2', required : false)
3 changes: 3 additions & 0 deletions test cases/unit/74 summary/subprojects/sub/meson.build
@@ -0,0 +1,3 @@
project('test summary subproject')

summary('Features', {'compiled': ['foo', 'bar']})
5 changes: 5 additions & 0 deletions 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')

0 comments on commit 0ccf4bb

Please sign in to comment.