| # -*- coding: utf-8 -*- |
| # Copyright 2019 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Parser for Portage elog summary.log files |
| |
| The summary.log file is created by Portage at ${PORT_LOGDIR}/elog/summary.log |
| and appended to by every run of `emerge`. Any package that emits information |
| via the `elog` system will have an entry in the summary log, even those with |
| fatal errors. |
| |
| The `ebuild` command and invocations of `emerge` with --pretend will *NOT* |
| generate entries in the summary.log file. |
| |
| Documentation for the Portage log system can be found at: |
| https://wiki.gentoo.org/wiki/Portage_log |
| """ |
| from __future__ import absolute_import |
| from __future__ import print_function |
| |
| import re |
| from collections import defaultdict |
| |
| import six |
| |
| from chromite.lib.parser import package_info |
| |
| SECTION_HEADER = re.compile( |
| r'>>> Messages generated by process (?P<process>\d+) on (?P<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \w{3}) for package (?P<package>[^:]+):' # pylint: disable=line-too-long |
| ) |
| |
| LOG_ENTRY = re.compile( |
| r'(?P<level>INFO|WARN|LOG|ERROR|QA): (?P<phase>setup|unpack|prepare|configure|compile|test|install|preinst|postinst)' # pylint: disable=line-too-long |
| ) |
| |
| |
| class Error(Exception): |
| """Base exception type for this parser""" |
| |
| |
| class MalformedLogError(Error): |
| """Error indicating a parsed log has unexpected structure""" |
| |
| |
| class DuplicatePackageError(Error): |
| """Error indicating that duplicate packages were found in a summary.log""" |
| |
| |
| class SummaryLog(object): |
| """Parsed contents of a summary.log file""" |
| |
| def __init__(self, log_contents): |
| self.package_logs = sorted( |
| (PackageLog(cpv, logs) for cpv, logs in six.iteritems(log_contents)), |
| key=lambda x: x.cpv) |
| |
| def has_failed_packages(self): |
| return any(log.has_errors() for log in self.package_logs) |
| |
| def failed_packages(self): |
| return sorted(log.cp for log in self.package_logs if log.has_errors()) |
| |
| def has_warned_packages(self): |
| return any(log.has_warnings() for log in self.package_logs) |
| |
| def warned_packages(self): |
| return sorted(log.cp for log in self.package_logs if log.has_warnings()) |
| |
| @staticmethod |
| def parse_from_file(file_path): |
| with open(file_path) as logfile: |
| return _parse_summary_log_from_lines_iterator(logfile) |
| |
| @staticmethod |
| def parse_from_string(string): |
| return _parse_summary_log_from_lines_iterator(string.splitlines(True)) |
| |
| |
| class PackageLog(object): |
| """Parsed contents of a single package's entry from a summary.log""" |
| |
| def __init__(self, cpv, log_mapping): |
| self.cpv = cpv |
| self.log_levels = dict((k, dict(v)) for k, v in six.iteritems(log_mapping)) |
| |
| @property |
| def cp(self): |
| return self.cpv.cp |
| |
| def has_errors(self): |
| return 'ERROR' in self.log_levels |
| |
| def has_warnings(self): |
| return 'WARN' in self.log_levels |
| |
| |
| def _parse_summary_log_from_lines_iterator(log_lines, no_duplicates=False): |
| cpv = None |
| level = None |
| phase = None |
| |
| # A mapping of package cpv -> log level -> ebuild phase -> messages |
| # Ex: contents["app-editor/vim"]["ERROR"]["compile"] -> "something happened" |
| contents = defaultdict(lambda: defaultdict(lambda: defaultdict(str))) |
| |
| for line in log_lines: |
| # Skip over blank lines |
| if not line.strip(): |
| continue |
| |
| # Update the state once we're in a log section for a particular package |
| # If we encounter the same package twice that means the log has entries |
| # from a previous attempt. |
| header_match = SECTION_HEADER.match(line.strip()) |
| if header_match: |
| cpv = package_info.SplitCPV(header_match.group('package')) |
| if cpv in contents and no_duplicates: |
| raise DuplicatePackageError() |
| continue |
| |
| # Match the ebuild phase and log entry. If we don't have a package set yet, |
| # then the log we're reading is malformed. |
| entry_match = LOG_ENTRY.match(line.strip()) |
| if entry_match: |
| if not cpv: |
| raise MalformedLogError() |
| phase = entry_match.group('phase') |
| level = entry_match.group('level') |
| continue |
| |
| # All other lines are then the actual messages printed by ebuilds. Append |
| # them to their respective categories. If anything is unset by this point, |
| # the log is again malformed. |
| if not all((cpv, phase, level)): |
| raise MalformedLogError() |
| contents[cpv][level][phase] += line |
| |
| return SummaryLog(contents) |