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 11, 2019
1 parent f2ad800 commit 8fbfb59
Show file tree
Hide file tree
Showing 9 changed files with 237 additions and 23 deletions.
1 change: 1 addition & 0 deletions data/syntax-highlighting/vim/syntax/meson.vim
Expand Up @@ -117,6 +117,7 @@ syn keyword mesonBuiltin
\ subdir
\ subdir_done
\ subproject
\ summary
\ target_machine
\ test
\ vcs_tag
Expand Down
56 changes: 56 additions & 0 deletions docs/markdown/Reference-manual.md
Expand Up @@ -1206,6 +1206,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
109 changes: 86 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 @@ -1751,6 +1751,50 @@ def method_call(self, method_name, args, kwargs):
raise InterpreterException('Extension module altered internal state illegally.')
return self.interpreter.module_method_callback(value)


class Summary:
def __init__(self):
self.sections = collections.defaultdict(dict)
self.max_key_width = 0

def add_section(self, section, values):
for k, v in values.items():
if k in self.sections[section]:
raise InterpreterException('Summary section {!r} already have key {!r}'.format(section, k))
if isinstance(v, list):
check_values = v
elif isinstance(v, dict):
check_values = v.values()
else:
check_values = [v]
for i in check_values:
if not isinstance(i, (str, int)):
raise InterpreterException()
self.sections[section][k] = v
self.max_key_width = max(self.max_key_width, max([len(i) for i in values.keys()]))

def dump(self):
for section, values in self.sections.items():
mlog.log('') # newline
mlog.log(' ', mlog.bold(section))
for k, v in values.items():
indent = self.max_key_width - len(k) + 3
if isinstance(v, list):
mlog.log(' ' * indent, k + ':', v[0])
indent += len(k) + 2
for i in v[1:]:
mlog.log(' ' * indent, i)
elif isinstance(v, dict):
v_items = list(v.items())
mlog.log(' ' * indent, k + ':', v_items[0][0], '=', v_items[0][1])
indent += len(k) + 2
for i in v_items[1:]:
mlog.log(' ' * indent, i[0], '=', i[1])
else:
mlog.log(' ' * indent, k + ':', v)
mlog.log('') # newline


class MesonMain(InterpreterObject):
def __init__(self, build, interpreter):
InterpreterObject.__init__(self)
Expand Down Expand Up @@ -2078,6 +2122,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(Summary)
if modules is None:
self.modules = {}
else:
Expand Down Expand Up @@ -2188,6 +2233,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 @@ -2594,6 +2640,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 @@ -2802,38 +2849,52 @@ 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.')
section, values = args
if not isinstance(section, str):
raise InterpreterException('Argument 1 must be a string.')
if not isinstance(values, dict):
raise InterpreterException('Argument 2 must be a dictionary.')
self.summary[self.subproject].add_section(section, values)

def _print_summary(self):
mlog.log('') # newline
for project, summary in sorted(self.summary.items()):
if project == '':
continue
subi = self.subprojects[project].held_object
project_name = self.build.projects[project]
project_version = subi.project_version
mlog.log(project_name, mlog.normal_cyan(project_version))
summary.dump()

if '' in self.summary:
mlog.log(self.build.project_name, mlog.normal_cyan(self.project_version))
self.summary[''].dump()

@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 @@ -4070,6 +4131,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
30 changes: 30 additions & 0 deletions run_unittests.py
Expand Up @@ -4128,6 +4128,36 @@ 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'''
Some Subproject 2.0
Features
foo: bar
My Project 1.0
Directories
prefix: /usr
bindir: bin
libdir: lib
datadir: share
Configuration
Some boolean: False
Another boolean: True
Some string: Hello World
A list: string
1
True
A dict: key1 = string
key2 = 1
key3 = True
'''))


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

sec1 = {'driver' : 'foobar'}
sec2 = {'driver' : 'dive comp', 'API' : '1.1.2'}
sec3 = {'with' : {'mesa' : true, 'gbm' : false}}


subproject('sub')
subproject('sub2', required : false)

summary('Directories', {'prefix': get_option('prefix'),
'bindir': get_option('bindir'),
'libdir': get_option('libdir'),
'datadir': get_option('datadir'),
})
summary('Configuration', {'Some boolean': false,
'Another boolean': true,
'Some string': 'Hello World',
'A list': ['string', 1, true],
'A dict': {'key1': 'string',
'key2': 1,
'key3': true}
})
3 changes: 3 additions & 0 deletions test cases/unit/74 summary/subprojects/sub/meson.build
@@ -0,0 +1,3 @@
project('Some Subproject', version : '2.0')

summary('Features', {'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 8fbfb59

Please sign in to comment.