Bug 873720 - Part 4: Display a build progress indicator; r=ted

This commit is contained in:
Gregory Szorc 2013-05-23 16:28:10 -07:00
parent ccf67001bd
commit a373571c12

View File

@ -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)