mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 873720 - Part 4: Display a build progress indicator; r=ted
This commit is contained in:
parent
ccf67001bd
commit
a373571c12
@ -7,6 +7,7 @@ from __future__ import print_function, unicode_literals
|
||||
import logging
|
||||
import operator
|
||||
import os
|
||||
import sys
|
||||
|
||||
from mach.decorators import (
|
||||
CommandArgument,
|
||||
@ -14,6 +15,8 @@ from mach.decorators import (
|
||||
Command,
|
||||
)
|
||||
|
||||
from mach.mixin.logging import LoggingMixin
|
||||
|
||||
from mozbuild.base import MachCommandBase
|
||||
|
||||
|
||||
@ -40,6 +43,209 @@ Preferences.
|
||||
'''.strip()
|
||||
|
||||
|
||||
class TerminalLoggingHandler(logging.Handler):
|
||||
"""Custom logging handler that works with terminal window dressing.
|
||||
|
||||
This class should probably live elsewhere, like the mach core. Consider
|
||||
this a proving ground for its usefulness.
|
||||
"""
|
||||
def __init__(self):
|
||||
logging.Handler.__init__(self)
|
||||
|
||||
self.fh = sys.stdout
|
||||
self.footer = None
|
||||
|
||||
def flush(self):
|
||||
self.acquire()
|
||||
|
||||
try:
|
||||
self.fh.flush()
|
||||
finally:
|
||||
self.release()
|
||||
|
||||
def emit(self, record):
|
||||
msg = self.format(record)
|
||||
|
||||
if self.footer:
|
||||
self.footer.clear()
|
||||
|
||||
self.fh.write(msg)
|
||||
self.fh.write('\n')
|
||||
|
||||
if self.footer:
|
||||
self.footer.draw()
|
||||
|
||||
# If we don't flush, the footer may not get drawn.
|
||||
self.flush()
|
||||
|
||||
|
||||
class BuildProgressFooter(object):
|
||||
"""Handles display of a build progress indicator in a terminal.
|
||||
|
||||
When mach builds inside a blessings-supported terminal, it will render
|
||||
progress information collected from a BuildMonitor. This class converts the
|
||||
state of BuildMonitor into terminal output.
|
||||
"""
|
||||
|
||||
def __init__(self, terminal, monitor):
|
||||
# terminal is a blessings.Terminal.
|
||||
self._t = terminal
|
||||
self._fh = sys.stdout
|
||||
self._monitor = monitor
|
||||
|
||||
def _clear_lines(self, n):
|
||||
for i in range(n):
|
||||
self._fh.write(self._t.move_x(0))
|
||||
self._fh.write(self._t.clear_eol())
|
||||
self._fh.write(self._t.move_up())
|
||||
|
||||
self._fh.write(self._t.move_down())
|
||||
self._fh.write(self._t.move_x(0))
|
||||
|
||||
def clear(self):
|
||||
"""Removes the footer from the current terminal."""
|
||||
self._clear_lines(1)
|
||||
|
||||
def draw(self):
|
||||
"""Draws this footer in the terminal."""
|
||||
if not self._monitor.tiers:
|
||||
return
|
||||
|
||||
# The drawn terminal looks something like:
|
||||
# TIER: base nspr nss js platform app SUBTIER: static export libs tools DIRECTORIES: 06/09 (memory)
|
||||
|
||||
# This is a list of 2-tuples of (encoding function, input). None means
|
||||
# no encoding. For a full reason on why we do things this way, read the
|
||||
# big comment below.
|
||||
parts = [('bold', 'TIER'), ':', ' ']
|
||||
|
||||
current_encountered = False
|
||||
for tier in self._monitor.tiers:
|
||||
if tier == self._monitor.current_tier:
|
||||
parts.extend([('yellow', tier), ' '])
|
||||
current_encountered = True
|
||||
elif not current_encountered:
|
||||
parts.extend([('green', tier), ' '])
|
||||
else:
|
||||
parts.extend([tier, ' '])
|
||||
|
||||
current_encountered = False
|
||||
parts.extend([('bold', 'SUBTIER'), ':', ' '])
|
||||
for subtier in self._monitor.subtiers:
|
||||
if subtier == self._monitor.current_subtier:
|
||||
parts.extend([('yellow', subtier), ' '])
|
||||
current_encountered = True
|
||||
elif not current_encountered:
|
||||
parts.extend([('green', subtier), ' '])
|
||||
else:
|
||||
parts.extend([subtier, ' '])
|
||||
|
||||
if self._monitor.current_subtier_dirs and self._monitor.current_tier_dir:
|
||||
parts.extend([
|
||||
('bold', 'DIRECTORIES'), ': ',
|
||||
'%02d' % self._monitor.current_tier_dir_index,
|
||||
'/',
|
||||
'%02d' % len(self._monitor.current_subtier_dirs),
|
||||
' ',
|
||||
'(', ('magenta', self._monitor.current_tier_dir), ')',
|
||||
])
|
||||
|
||||
# We don't want to write more characters than the current width of the
|
||||
# terminal otherwise wrapping may result in weird behavior. We can't
|
||||
# simply truncate the line at terminal width characters because a)
|
||||
# non-viewable escape characters count towards the limit and b) we
|
||||
# don't want to truncate in the middle of an escape sequence because
|
||||
# subsequent output would inherit the escape sequence.
|
||||
max_width = self._t.width
|
||||
written = 0
|
||||
write_pieces = []
|
||||
for part in parts:
|
||||
if isinstance(part, tuple):
|
||||
func, arg = part
|
||||
|
||||
if written + len(arg) > max_width:
|
||||
write_pieces.append(arg[0:max_width - written])
|
||||
written += len(arg)
|
||||
break
|
||||
|
||||
encoded = getattr(self._t, func)(arg)
|
||||
|
||||
write_pieces.append(encoded)
|
||||
written += len(arg)
|
||||
else:
|
||||
if written + len(part) > max_width:
|
||||
write_pieces.append(arg[0:max_width - written])
|
||||
written += len(part)
|
||||
break
|
||||
|
||||
write_pieces.append(part)
|
||||
written += len(part)
|
||||
|
||||
self._fh.write(''.join(write_pieces))
|
||||
self._fh.flush()
|
||||
|
||||
|
||||
class BuildOutputManager(LoggingMixin):
|
||||
"""Handles writing build output to a terminal, to logs, etc."""
|
||||
|
||||
def __init__(self, log_manager, monitor):
|
||||
self.populate_logger()
|
||||
|
||||
self.monitor = monitor
|
||||
self.footer = None
|
||||
|
||||
terminal = log_manager.terminal
|
||||
|
||||
# TODO convert terminal footer to config file setting.
|
||||
if not terminal or os.environ.get('MACH_NO_TERMINAL_FOOTER', None):
|
||||
return
|
||||
|
||||
self.t = terminal
|
||||
self.footer = BuildProgressFooter(terminal, monitor)
|
||||
|
||||
handler = TerminalLoggingHandler()
|
||||
handler.setFormatter(log_manager.terminal_formatter)
|
||||
handler.footer = self.footer
|
||||
|
||||
old = log_manager.replace_terminal_handler(handler)
|
||||
handler.level = old.level
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
if self.footer:
|
||||
self.footer.clear()
|
||||
|
||||
def write_line(self, line):
|
||||
if self.footer:
|
||||
self.footer.clear()
|
||||
|
||||
print(line)
|
||||
|
||||
if self.footer:
|
||||
self.footer.draw()
|
||||
|
||||
def refresh(self):
|
||||
if not self.footer:
|
||||
return
|
||||
|
||||
self.footer.clear()
|
||||
self.footer.draw()
|
||||
|
||||
def on_line(self, line):
|
||||
warning, state_changed, relevant = self.monitor.on_line(line)
|
||||
|
||||
if warning:
|
||||
self.log(logging.INFO, 'compiler_warning', warning,
|
||||
'Warning: {flag} in {filename}: {message}')
|
||||
|
||||
if relevant:
|
||||
self.log(logging.INFO, 'build_output', {'line': line}, '{line}')
|
||||
elif state_changed:
|
||||
self.refresh()
|
||||
|
||||
|
||||
@CommandProvider
|
||||
class Build(MachCommandBase):
|
||||
"""Interface to build the tree."""
|
||||
@ -60,73 +266,65 @@ class Build(MachCommandBase):
|
||||
warnings_path = self._get_state_filename('warnings.json')
|
||||
monitor = BuildMonitor(self.topobjdir, warnings_path)
|
||||
|
||||
def on_line(line):
|
||||
warning, state_changed, relevant = monitor.on_line(line)
|
||||
with BuildOutputManager(self.log_manager, monitor) as output:
|
||||
monitor.start()
|
||||
|
||||
if warning:
|
||||
self.log(logging.INFO, 'compiler_warning', warning,
|
||||
'Warning: {flag} in {filename}: {message}')
|
||||
|
||||
if relevant:
|
||||
self.log(logging.INFO, 'build_output', {'line': line}, '{line}')
|
||||
|
||||
monitor.start()
|
||||
|
||||
if what:
|
||||
top_make = os.path.join(self.topobjdir, 'Makefile')
|
||||
if not os.path.exists(top_make):
|
||||
print('Your tree has not been configured yet. Please run '
|
||||
'|mach build| with no arguments.')
|
||||
return 1
|
||||
|
||||
# Collect target pairs.
|
||||
target_pairs = []
|
||||
for target in what:
|
||||
path_arg = self._wrap_path_argument(target)
|
||||
|
||||
make_dir, make_target = resolve_target_to_make(self.topobjdir,
|
||||
path_arg.relpath())
|
||||
|
||||
if make_dir is None and make_target is None:
|
||||
if what:
|
||||
top_make = os.path.join(self.topobjdir, 'Makefile')
|
||||
if not os.path.exists(top_make):
|
||||
print('Your tree has not been configured yet. Please run '
|
||||
'|mach build| with no arguments.')
|
||||
return 1
|
||||
|
||||
target_pairs.append((make_dir, make_target))
|
||||
# Collect target pairs.
|
||||
target_pairs = []
|
||||
for target in what:
|
||||
path_arg = self._wrap_path_argument(target)
|
||||
|
||||
# Possibly add extra make depencies using dumbmake.
|
||||
if not disable_extra_make_dependencies:
|
||||
from dumbmake.dumbmake import (dependency_map,
|
||||
add_extra_dependencies)
|
||||
depfile = os.path.join(self.topsrcdir, 'build',
|
||||
'dumbmake-dependencies')
|
||||
with open(depfile) as f:
|
||||
dm = dependency_map(f.readlines())
|
||||
new_pairs = list(add_extra_dependencies(target_pairs, dm))
|
||||
self.log(logging.DEBUG, 'dumbmake',
|
||||
{'target_pairs': target_pairs,
|
||||
'new_pairs': new_pairs},
|
||||
'Added extra dependencies: will build {new_pairs} ' +
|
||||
'instead of {target_pairs}.')
|
||||
target_pairs = new_pairs
|
||||
make_dir, make_target = resolve_target_to_make(self.topobjdir,
|
||||
path_arg.relpath())
|
||||
|
||||
# Build target pairs.
|
||||
for make_dir, make_target in target_pairs:
|
||||
status = self._run_make(directory=make_dir, target=make_target,
|
||||
line_handler=on_line, log=False, print_directory=False,
|
||||
ensure_exit_code=False, num_jobs=jobs, silent=not verbose)
|
||||
if make_dir is None and make_target is None:
|
||||
return 1
|
||||
|
||||
if status != 0:
|
||||
break
|
||||
else:
|
||||
status = self._run_make(srcdir=True, filename='client.mk',
|
||||
line_handler=on_line, log=False, print_directory=False,
|
||||
allow_parallel=False, ensure_exit_code=False, num_jobs=jobs,
|
||||
silent=not verbose)
|
||||
target_pairs.append((make_dir, make_target))
|
||||
|
||||
self.log(logging.WARNING, 'warning_summary',
|
||||
{'count': len(monitor.warnings_database)},
|
||||
'{count} compiler warnings present.')
|
||||
# Possibly add extra make depencies using dumbmake.
|
||||
if not disable_extra_make_dependencies:
|
||||
from dumbmake.dumbmake import (dependency_map,
|
||||
add_extra_dependencies)
|
||||
depfile = os.path.join(self.topsrcdir, 'build',
|
||||
'dumbmake-dependencies')
|
||||
with open(depfile) as f:
|
||||
dm = dependency_map(f.readlines())
|
||||
new_pairs = list(add_extra_dependencies(target_pairs, dm))
|
||||
self.log(logging.DEBUG, 'dumbmake',
|
||||
{'target_pairs': target_pairs,
|
||||
'new_pairs': new_pairs},
|
||||
'Added extra dependencies: will build {new_pairs} ' +
|
||||
'instead of {target_pairs}.')
|
||||
target_pairs = new_pairs
|
||||
|
||||
# Build target pairs.
|
||||
for make_dir, make_target in target_pairs:
|
||||
status = self._run_make(directory=make_dir, target=make_target,
|
||||
line_handler=output.on_line, log=False, print_directory=False,
|
||||
ensure_exit_code=False, num_jobs=jobs, silent=not verbose)
|
||||
|
||||
if status != 0:
|
||||
break
|
||||
else:
|
||||
status = self._run_make(srcdir=True, filename='client.mk',
|
||||
line_handler=output.on_line, log=False, print_directory=False,
|
||||
allow_parallel=False, ensure_exit_code=False, num_jobs=jobs,
|
||||
silent=not verbose)
|
||||
|
||||
self.log(logging.WARNING, 'warning_summary',
|
||||
{'count': len(monitor.warnings_database)},
|
||||
'{count} compiler warnings present.')
|
||||
|
||||
monitor.finish()
|
||||
|
||||
monitor.finish()
|
||||
high_finder, finder_percent = monitor.have_high_finder_usage()
|
||||
if high_finder:
|
||||
print(FINDER_SLOW_MESSAGE % finder_percent)
|
||||
|
Loading…
Reference in New Issue
Block a user